Source: Acl.js

'use strict'

const { DENY, PERMIT } = require('./StaticAccessControlStrategy')
const Ace = require('./Ace')

/**
 * Access control list
 */
class Acl {
  constructor () {
    this._aces = []
  }

  /**
   * Returns whether all of the given actions are granted amongst the given principals against the given securable, and none of the principals are denied any of the actions.
   *
   * @param {Object} arg The argument to be deconstructed.
   * @param {any[]} arg.principals The **principals** that must individually not be explicitly denied `arg.actions` and that must collectively be granted `arg.actions`.
   * @param {any[]} arg.actions The actions none of which must be denied from individual `arg.principals` and all of which must be collectively granted amongst `arg.principals`.
   * @param {any} arg.securable The thing access to which is being governed.
   * @param {any} [arg.data] Any contextual information needed for the access control decision; passed into each ACE's `denies` & `grants` methods.
   * @return {boolean} Whether the `arg.principals` are collectively granted.
   * @type {MultiGrantsDecisionFn}
   * @since 1.3.0
   */
  permits ({ principals, actions, securable, data }) {
    principals = this._ensureArray(principals)
    actions = this._ensureArray(actions)

    const aces = this._findApplicableAces({ principals, actions, securable })
    if (this._denies({ principals, actions, securable, data, aces })) return false
    // else no one's denied, so check for grants

    // start with each unique action not being granted
    const grantsByAction = actions.reduce((grants, action) => {
      grants[action] = false
      return grants
    }, {})

    aces.forEach(ace => {
      principals.forEach(principal => {
        actions.forEach(action => {
          // skip if action is already granted
          if (!grantsByAction[action] && ace.grants({ principal, action, securable, data })) {
            grantsByAction[action] = true
          }
        })
      })
    })

    return !actions.map(action => grantsByAction[action]).includes(false) // whether any actions weren't granted
  }

  /**
   * Returns whether all of the given actions are granted amongst the given principals against the given securable, and none of the principals are denied any of the actions.
   *
   * @param {Object} arg The argument to be deconstructed.
   * @param {any[]} arg.principals The **principals** that must individually not be explicitly denied `arg.actions` and that must collectively be granted `arg.actions`.
   * @param {any[]} arg.actions The actions none of which must be denied from individual `arg.principals` and all of which must be collectively granted amongst `arg.principals`.
   * @param {any} arg.securable The thing access to which is being governed.
   * @param {any} [arg.data] Any contextual information needed for the access control decision; passed into each ACE's `denies` & `grants` methods.
   * @return {boolean} Whether the `arg.principals` are collectively granted.
   * @type {MultiGrantsDecisionFn}
   * @deprecated use Acl#permits
   */
  grants ({ principals, actions, securable, data }) {
    return this.permits(...arguments)
  }

  /**
   * Returns whether any of the given principals are explicitly denied any of the given actions against the given securable.
   * @param {Object} arg The argument to be deconstructed.
   * @param {any[]} arg.principals The principals that are individually tested for explicit denial of any of the `arg.actions`.
   * @param {any[]} arg.actions The actions none of which must be explicitly denied from `arg.principals`.
   * @param {any} arg.securable The thing access to which is being governed.
   * @param {any} [arg.data] Any contextual information needed for the access control decision; passed to each ACE's `denies` & `grants` methods.
   * @return {boolean} Whether any of the `arg.principals` are denied any of the (@param actions}.
   */
  denies ({ principals, actions, securable, data }) {
    principals = this._ensureArray(principals)
    actions = this._ensureArray(actions)

    return this._denies({
      principals,
      actions,
      securable,
      data,
      aces: this._findApplicableAces({ principals, actions, securable })
    })
  }

  _denies ({ principals, actions, securable, data, aces }) {
    principals = this._ensureArray(principals)
    actions = this._ensureArray(actions)

    for (const ace of aces) {
      for (const principal of principals) {
        for (const action of actions) {
          if (ace.denies({ principal, action, securable, data })) return true
        }
      }
    }
    return false
  }

  _findApplicableAces ({ principals, actions, securable }) {
    return this._aces.filter(ace => {
      let applies = false
      for (const p of principals) {
        if (ace.appliesToPrincipal(p)) {
          applies = true
          break
        }
      }
      if (!applies) return false

      applies = false
      for (const a of actions) {
        if (ace.appliesToAction(a)) {
          applies = true
          break
        }
      }
      if (!applies) return false

      return ace.appliesToSecurable(securable)
    })
  }

  _ensureArray (it) {
    return Array.isArray(it) ? it : [it]
  }

  /**
   * @since 1.3.0
   */
  permit ({ principal, securable, action }) {
    return this.secure({ strategy: PERMIT, principal, securable, action })
  }

  /**
   * @deprecated use Acl#permit
   */
  grant ({ principal, securable, action }) {
    return this.permit(...arguments)
  }

  /**
   * @since 1.3.0
   */
  unpermit ({ principal, securable, action }) {
    return this.unsecure({ strategy: PERMIT, principal, securable, action })
  }

  /**
   * @deprecated use Acl#unpermit
   */
  ungrant ({ principal, securable, action }) {
    return this.unpermit(...arguments)
  }

  deny ({ principal, securable, action }) {
    return this.secure({ strategy: DENY, principal, securable, action })
  }

  undeny ({ principal, securable, action }) {
    return this.unsecure({ strategy: DENY, principal, securable, action })
  }

  /**
   * Adds an access control entry (ACE).
   * Idempotent if the ACE already exists.
   *
   * @param {Object} arg The argument to be deconstructed.
   * @param {AccessControlStrategy} arg.strategy The ACE's strategy.
   * @param {any} arg.principal The ACE's principal.
   * @param {any} arg.securable The ACE's securable.
   * @param {any} arg.action The ACE's action.
   * @return {Acl} This ACL in order to support a builder pattern.
   */
  secure ({ strategy, principal, securable, action }) {
    return this._secure({ strategy, principal, securable, action, add: true })
  }

  /**
   * Removes an access control entry (ACE).
   * Idempotent if the ACE already absent.
   *
   * @param {Object} arg The argument to be deconstructed.
   * @param {AccessControlStrategy} arg.strategy The ACE's strategy.
   * @param {any} arg.principal The ACE's principal.
   * @param {any} arg.securable The ACE's securable.
   * @param {any} arg.action The ACE's action.
   * @return {Acl} This ACL in order to support a builder pattern.
   */
  unsecure ({ strategy, principal, securable, action }) {
    return this._secure({ strategy, principal, securable, action, add: false })
  }

  _secure ({ strategy, principal, securable, action, add = true }) {
    const index = this._aces.findIndex(ace =>
      ace.appliesToPrincipal(principal) &&
      ace.appliesToAction(action) &&
      ace.appliesToStrategy(strategy) &&
      (securable ? ace.appliesToSecurable(securable) : true)
    )

    if (add && index === -1) { // not found, so add, else ignore because it's already there
      this._aces.push(Ace.of({ strategy, principal, securable, action }))
    } else if (!add && index !== -1) { // found, so remove, else ignore because it's already not there
      this._aces.splice(index, 1)
    }

    return this
  }
}

module.exports = Acl