Source: Ace.js

'use strict'

const { GRANT, DENY, PERMIT } = require('./StaticAccessControlStrategy')
const DEFAULT_SAMENESS_TESTER = require('./SamenessTester')

/**
 * An access control entry, which binds together
 * * an **access control strategy**,
 * * a **principal**,
 * * a **securable**, and
 * * an **action**.
 *
 * You may also provide a **sameness tester function** to determine sameness amongst objects, defaulting to {@link Ace.testDefaultSameness}.
 */
class Ace {
  /**
   * A factory method for producing an {@link Ace} that statically permits the principal to take the action with respect to the securable.
   * @param {Object} arg The argument.
   * @param {AccessControlStrategy} arg.strategy The {@link Ace}'s strategy.
   * @param {any} arg.principal The {@link Ace}'s principal.
   * @param {any} arg.securable The {@link Ace}'s securable.
   * @param {any} arg.action The {@link Ace}'s action.
   * @param {SamenessTesterFn} [arg.samenessTesterFn] A function used to determine if two objects are equivalent, defaulting to {@link Ace.testDefaultSameness}.
   * @return {Ace}
   * @since 1.3.0
   */
  static permitting ({ principal, securable, action, samenessTesterFn } = {}) {
    return Ace.of({ strategy: PERMIT, principal, securable, action, samenessTesterFn })
  }

  /**
   * A factory method for producing an {@link Ace} that statically grants the action to the principal with respect to the securable.
   * @param {Object} arg The argument.
   * @param {AccessControlStrategy} arg.strategy The {@link Ace}'s strategy.
   * @param {any} arg.principal The {@link Ace}'s principal.
   * @param {any} arg.securable The {@link Ace}'s securable.
   * @param {any} arg.action The {@link Ace}'s action.
   * @param {SamenessTesterFn} [arg.samenessTesterFn] A function used to determine if two objects are equivalent, defaulting to {@link Ace.testDefaultSameness}.
   * @return {Ace}
   * @deprecated use Ace.permitting
   */
  static granting ({ principal, securable, action, samenessTesterFn } = {}) {
    return Ace.permitting(...arguments)
  }

  /**
   * A factory method for producing an {@link Ace} that statically denies the action to the principal with respect to the securable.
   * @param {Object} arg The argument.
   * @param {AccessControlStrategy} arg.strategy The {@link Ace}'s strategy.
   * @param {any} arg.principal The {@link Ace}'s principal.
   * @param {any} arg.securable The {@link Ace}'s securable.
   * @param {any} arg.action The {@link Ace}'s action.
   * @param {SamenessTesterFn} [arg.samenessTesterFn] A function used to determine if two objects are equivalent, defaulting to {@link Ace.testDefaultSameness}.
   * @return {Ace}
   */
  static denying ({ principal, securable, action, samenessTesterFn } = {}) {
    return Ace.of({ strategy: DENY, principal, securable, action, samenessTesterFn })
  }

  /**
   * A factory method for producing an {@link Ace} that uses the given strategy to grant or deny the action to or from the principal, respectively, with respect to the securable.
   * @param {Object} arg The argument.
   * @param {AccessControlStrategy} arg.strategy The {@link Ace}'s strategy.
   * @param {any} arg.principal The {@link Ace}'s principal.
   * @param {any} arg.securable The {@link Ace}'s securable.
   * @param {any} arg.action The {@link Ace}'s action.
   * @param {SamenessTesterFn} [arg.samenessTesterFn] A function used to determine if two objects are equivalent, defaulting to {@link Ace.testDefaultSameness}.
   * @return {Ace}
   */
  static of ({ strategy, principal, securable, action, samenessTesterFn } = {}) {
    return new Ace({ strategy, principal, securable, action, samenessTesterFn })
  }

  static testDefaultSameness (that, other) {
    return DEFAULT_SAMENESS_TESTER(that, other)
  }

  /**
   * Constructor that uses the given strategy to grant or deny the action to or from the principal, respectively, with respect to the securable.
   * @param {Object} arg The argument.
   * @param {AccessControlStrategy} arg.strategy The {@link Ace}'s strategy.
   * @param {any} arg.principal The {@link Ace}'s principal.
   * @param {any} arg.securable The {@link Ace}'s securable.
   * @param {any} arg.action The {@link Ace}'s action.
   * @param {SamenessTesterFn} [arg.samenessTesterFn] A function used to determine if two objects are equivalent, defaulting to {@link Ace.testDefaultSameness}.
   */
  constructor ({ strategy, principal, securable, action, samenessTesterFn = Ace.testDefaultSameness }) {
    this._principal = principal
    this._action = action
    this._securable = securable
    this._strategy = this._testSetStrategy(strategy)
    this._testSameness = samenessTesterFn

    Object.freeze(this)
  }

  get principal () {
    return this._principal
  }

  get action () {
    return this._action
  }

  get securable () {
    return this._securable
  }

  get strategy () {
    return this._strategy
  }

  _testSetStrategy (strategy) {
    if (
      (strategy?.permits && typeof strategy?.permits !== 'function') ||
      (strategy?.grants && typeof strategy?.grants !== 'function') ||
      (!strategy?.grants && !strategy?.permits) ||
      typeof strategy?.denies !== 'function'
    ) {
      throw new Error('invalid strategy given')
    }
    return strategy
  }

  /**
   * Determines if this Ace applies to the given principal, securable, and action.
   * @private
   */
  _applies ({ principal, securable, action }) {
    return (
      this.appliesToSecurable(securable) &&
      this.appliesToAction(action) &&
      (!this._principal || this._testSameness(this._principal, principal))
    )
  }

  /**
   * Returns whether this {@link Ace} permits and does not explicitly deny the given **principal** the **action** with respect to the given **securable**, given optional **data**.
   *
   * @param {AccessControlTuple} arg The argument to be deconstructed.
   * @param {any} arg.principal The principal in question.
   * @param {any} arg.action The action in question.
   * @param {any} arg.securable The securable in question.
   * @param {any} [arg.data] Optional contextual data.
   * @since 1.3.0
   */
  permits ({ principal, action, securable, data } = {}) {
    return (
      this._applies({ principal, securable, action }) &&
      !this._strategy.denies({ principal, action, securable, data }) &&
      (this._strategy.permits || this._strategy.grants).bind(this._strategy)({ principal, action, securable, data })
    )
  }

  /**
   * Returns whether this {@link Ace} grants and does not explicitly deny the given **principal** the **action** with respect to the given **securable**, given optional **data**.
   *
   * @param {AccessControlTuple} arg The argument to be deconstructed.
   * @param {any} arg.principal The principal in question.
   * @param {any} arg.action The action in question.
   * @param {any} arg.securable The securable in question.
   * @param {any} [arg.data] Optional contextual data.
   * @deprecated use Ace#permits
   */
  grants ({ principal, action, securable, data } = {}) {
    return this.permits(...arguments)
  }

  /**
   * Returns whether this {@link Ace} explicitly denies the given **principal** the **action** with respect to the given **securable**, given optional **data**.
   *
   * @param {AccessControlTuple} arg The argument to be deconstructed.
   * @param {any} arg.principal The principal in question.
   * @param {any} arg.action The action in question.
   * @param {any} arg.securable The securable in question.
   * @param {any} [arg.data] Optional contextual data.
   */
  denies ({ principal, action, securable, data } = {}) {
    return (
      this._applies({ principal, securable, action }) &&
      this._strategy.denies({ principal, action, securable, data })
    )
  }

  appliesToPrincipal (principal) {
    return !this._principal || this._testSameness(principal, this._principal)
  }

  appliesToAction (action) {
    return !this._action || this._testSameness(action, this._action)
  }

  appliesToSecurable (securable) {
    return !this._securable || this._testSameness(securable, this._securable)
  }

  appliesToStrategy (strategy) {
    strategy = strategy === GRANT ? PERMIT : strategy
    return this._testSameness(strategy, this._strategy)
  }
}

Ace.DEFAULT_SAMENESS_TESTER = DEFAULT_SAMENESS_TESTER

module.exports = Ace