Adapt authoring tool UI documentation

v1.0.0-rc.4

adapt-authoring-auth/lib/Permissions.js

  1. import { App } from 'adapt-authoring-core'
  2. import AuthUtils from './AuthUtils.js'
  3. import { pathToRegexp } from 'path-to-regexp'
  4. /**
  5. * Handles checking user permissions for app endpoints
  6. * @memberof auth
  7. */
  8. class Permissions {
  9. /**
  10. * Creates and instanciates the class
  11. * @return {Promise} Resolves with the instance
  12. */
  13. static async init () {
  14. return new Permissions()
  15. }
  16. /** @constructor */
  17. constructor () {
  18. /**
  19. * Reference to all secured routes. Note that any route not explicitly secured will be denied by default.
  20. * @type {RouteStore}
  21. * @example
  22. * {
  23. * post: { "/api/test": true }
  24. * }
  25. */
  26. this.routes = AuthUtils.createEmptyStore()
  27. App.instance.onReady().then(this.checkRoutes.bind(this))
  28. }
  29. /**
  30. * 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)
  31. * @param {App} app The app instance
  32. * @return {Promise}
  33. */
  34. async checkRoutes (app) {
  35. const [auth, server] = await app.waitForModule('auth', 'server')
  36. if (!auth.getConfig('logMissingPermissions')) {
  37. return
  38. }
  39. const missing = []
  40. server.api.flattenRouters().forEach(router => {
  41. router.routes.forEach(routeConfig => {
  42. const route = `${router.path}${routeConfig.route}`
  43. const shortRoute = route.slice(0, route.lastIndexOf('/'))
  44. Object.keys(routeConfig.handlers).forEach(method => {
  45. const isUnsecure = auth.unsecuredRoutes[method][route] || auth.unsecuredRoutes[method][shortRoute]
  46. const hasPermissions = !!this.getScopesForRoute(method, route)
  47. if (!isUnsecure && !hasPermissions) missing.push({ route, method })
  48. })
  49. })
  50. })
  51. missing.forEach(({ route, method }) => log('warn', `no permissions specified for ${method.toUpperCase()} ${route}`))
  52. }
  53. /**
  54. * Restricts access to a route/endpoint
  55. * @note All endpoints are blocked by default
  56. * @type {Function}
  57. * @param {String} route The route/endpoint to secure
  58. * @param {String} method HTTP method to block
  59. * @param {Array} scopes The scopes to restrict
  60. */
  61. secureRoute (route, method, scopes) {
  62. const m = method.toLowerCase()
  63. const { regexp: re } = pathToRegexp(route)
  64. if (this.routes[m][re]) {
  65. return log('warn', `Route ${m} '${route}' already secured`)
  66. }
  67. this.routes[m].push([re, scopes])
  68. }
  69. /**
  70. * Returns the scopes needed for a specific route
  71. * @param {String} method HTTP method
  72. * @param {String} route The route to check
  73. * @returns {Array} the scopes required for route
  74. */
  75. getScopesForRoute (method, route) {
  76. for (const [re, scopes] of this.routes[method]) {
  77. if (re.test(route)) return scopes
  78. }
  79. }
  80. /**
  81. * Checks incoming request against stored permissions
  82. * @param {external:ExpressRequest} req
  83. * @return {Promise} Resolves if request user passes checks
  84. */
  85. async check (req) {
  86. const route = `${req.baseUrl}${req.path.endsWith('/') ? req.path.slice(0, -1) : req.path}`
  87. const userScopes = req.auth.scopes || []
  88. const neededScopes = this.getScopesForRoute(req.method.toLowerCase(), route)
  89. if (!neededScopes) {
  90. log('warn', `blocked access to route with no permissions '${route}'`)
  91. }
  92. if (!req.auth.isSuper && !neededScopes?.every(s => userScopes.includes(s))) {
  93. throw App.instance.errors.UNAUTHORISED
  94. .setData({ method: req.method, url: route })
  95. }
  96. }
  97. }
  98. /** @ignore */
  99. async function log (...args) {
  100. const auth = await App.instance.waitForModule('auth')
  101. auth.log(...args)
  102. }
  103. export default Permissions