adapt-authoring-auth/lib/Permissions.js

import { App } from 'adapt-authoring-core'
import AuthUtils from './AuthUtils.js'
import { pathToRegexp } from 'path-to-regexp'
/**
 * Handles checking user permissions for app endpoints
 * @memberof auth
 */
class Permissions {
  /**
   * Creates and instanciates the class
   * @return {Promise} Resolves with the instance
   */
  static async init () {
    return new Permissions()
  }

  /** @constructor */
  constructor () {
    /**
     * Reference to all secured routes. Note that any route not explicitly secured will be denied by default.
     * @type {RouteStore}
     * @example
     * {
     *   post: { "/api/test": true }
     * }
     */
    this.routes = AuthUtils.createEmptyStore()

    App.instance.onReady().then(this.checkRoutes.bind(this))
  }

  /**
   * Checks for routes which don't have valid permissions set, and logs a warning message (as these routes will not be accessible from the API)
   * @param {App} app The app instance
   * @return {Promise}
   */
  async checkRoutes (app) {
    const [auth, server] = await app.waitForModule('auth', 'server')

    if (!auth.getConfig('logMissingPermissions')) {
      return
    }
    const missing = []
    server.api.flattenRouters().forEach(router => {
      router.routes.forEach(routeConfig => {
        const route = `${router.path}${routeConfig.route}`
        const shortRoute = route.slice(0, route.lastIndexOf('/'))
        Object.keys(routeConfig.handlers).forEach(method => {
          const isUnsecure = auth.unsecuredRoutes[method][route] || auth.unsecuredRoutes[method][shortRoute]
          const hasPermissions = !!this.getScopesForRoute(method, route)
          if (!isUnsecure && !hasPermissions) missing.push({ route, method })
        })
      })
    })
    missing.forEach(({ route, method }) => log('warn', `no permissions specified for ${method.toUpperCase()} ${route}`))
  }

  /**
   * Restricts access to a route/endpoint
   * @note All endpoints are blocked by default
   * @type {Function}
   * @param {String} route The route/endpoint to secure
   * @param {String} method HTTP method to block
   * @param {Array} scopes The scopes to restrict
   */
  secureRoute (route, method, scopes) {
    const m = method.toLowerCase()
    const { regexp: re } = pathToRegexp(route)
    if (this.routes[m][re]) {
      return log('warn', `Route ${m} '${route}' already secured`)
    }
    this.routes[m].push([re, scopes])
  }

  /**
   * Returns the scopes needed for a specific route
   * @param {String} method HTTP method
   * @param {String} route The route to check
   * @returns {Array} the scopes required for route
   */
  getScopesForRoute (method, route) {
    for (const [re, scopes] of this.routes[method]) {
      if (re.test(route)) return scopes
    }
  }

  /**
   * Checks incoming request against stored permissions
   * @param {external:ExpressRequest} req
   * @return {Promise} Resolves if request user passes checks
   */
  async check (req) {
    const route = `${req.baseUrl}${req.path.endsWith('/') ? req.path.slice(0, -1) : req.path}`
    const userScopes = req.auth.scopes || []
    const neededScopes = this.getScopesForRoute(req.method.toLowerCase(), route)
    if (!neededScopes) {
      log('warn', `blocked access to route with no permissions '${route}'`)
    }
    if (!req.auth.isSuper && !neededScopes?.every(s => userScopes.includes(s))) {
      throw App.instance.errors.UNAUTHORISED
        .setData({ method: req.method, url: route })
    }
  }
}
/** @ignore */
async function log (...args) {
  const auth = await App.instance.waitForModule('auth')
  auth.log(...args)
}

export default Permissions