mozilla/publish.webmaker.org

View on GitHub
api/classes/prerequisites.js

Summary

Maintainability
C
1 day
Test Coverage
"use strict";

const Boom = require(`boom`);
const Promise = require(`bluebird`);

const Errors = require(`./errors`);

class Prerequisites {
  /**
  * confirmRecordExists(model[, mode, requestKey, databasekey])
  *
  * Returns a HAPI pre-req package configured to
  * to fetch all matching records of the passed `model`,
  * using data from the route parameters or the request payload
  * to build the query.
  *
  * @param {Object} model - Bookshelf model for querying
  * @param {Object} config - Contains configuration options for the query:
  *     mode {String} - 'param' or 'payload' (if omitted, returns all records)
  *     requestKey {String} - key of the param/payload
  *     databaseKey {String} - key of the database model
  *
  * @return {Promise} - Promise fullfilled when the record is found
  */
  static confirmRecordExists(model, config) {
    config = config || {};
    const dbKey = config.databaseKey || config.requestKey;
    const requestKey = config.requestKey;

    function getRecordsFromDB(requestKeyValue) {
      const queryOptions = {};

      if (requestKey) {
        queryOptions.where = {
          [ dbKey ]: requestKeyValue
        };
      }

      const fetchOptions = {};

      if (config.columns) {
        fetchOptions.columns = config.columns;
      }

      return model.query(queryOptions).fetchAll(fetchOptions);
    }

    function getRecordsFromCache(server, requestKeyValue) {
      const cacheConfig = config.cache;
      const cacheMethodName = cacheConfig.methodName;
      const cacheMethod = server.methods[cacheMethodName];

      return Promise.fromCallback(next => {

        return cacheMethod.call(
          server.app.cacheContexts[cacheMethodName],
          requestKeyValue,
          next
        );
      })
      .then(cachedValue => cacheConfig.postProcess(requestKeyValue, cachedValue));
    }

    return {
      assign: `records`,
      method(req, reply) {
        const result = Promise.resolve()
        .then(function() {
          const requestKeyValue = requestKey && config.mode === `param` ? req.params[requestKey] : req.payload[requestKey];

          if (config.cache) {
            return getRecordsFromCache(req.server, requestKeyValue);
          }

          return getRecordsFromDB(requestKeyValue);
        })
        .then(function(records) {
          if (!records || records.length === 0) {
            throw Boom.notFound(null, {
              debug: true,
              error: `resource not found`
            });
          }

          return records;
        })
        .catch(Errors.generateErrorResponse);

        return reply(result);
      }
    };
  }

  /**
  * validateUser()
  *
  * Ensures that the user sending the request exists in the
  * current context. This means that the user should have hit the
  * /users/login route first
  *
  * @return {Promise} - Promise fullfilled when the user has been found
  */
  static validateUser() {
    return {
      assign: `user`,
      method(request, reply) {
        const usernameParam = request.auth.credentials.username;

        const result = Promise.fromCallback(next => {
          return request.server.methods.user(usernameParam, next);
        })
        .then(authenticatedUser => {
          if (!authenticatedUser) {
            // This case means our auth logic failed unexpectedly
            throw Boom.badImplementation(null, {
              error: `authenticated user doesn't exist (mayday!)`
            });
          }

          return authenticatedUser;
        })
        .catch(Errors.generateErrorResponse);

        return reply(result);
      }
    };
  }

  /**
  * validateOwnership([recordsAreNotModels, fetchUser, userIsNotModel])
  *
  * Ensures the authenticated user is the owner of the
  * resource being manipulated or requested.
  *
  * @param {boolean} recordsAreNotModels - If true, indicates that the records
  * contained in the request prerequisites are not Bookshelf Models but simple
  * javascript object representations of them
  * @param {Function} fetchUser - The function to use to fetch the user that
  * owns the resource represented by the simple javascript object in the
  * request prerequisites. This function is required if recordsAreNotModels
  * is set to true
  * @param {boolean} userIsNotModel - If true, indicates that the user returned
  * by the `fetchUser` function is not a Bookshelf Model but a simple
  * javascript object representation of one.
  *
  * @return {Promise} - Promise fullfilled when the user has been confirmed to
  * be the owner of the resource requested
  */
  static validateOwnership(recordsAreNotModels, fetchUser, userIsNotModel) {
    if (recordsAreNotModels && typeof fetchUser !== `function`) {
      throw new Error(`fetchUser needs to be a function provided if recordsAreNotModels is set to true`);
    }

    return {
      method(request, reply) {
        const records = request.pre.records;
        const resource = recordsAreNotModels ? records[0] : records.models[0];
        const authenticatedUser = request.pre.user;

        const result = Promise.resolve()
        .then(function() {
          if (typeof fetchUser === `function`) {
            return fetchUser(resource, request.server);
          }

          // Check if the resource is the owning user, otherwise fetch
          // the user it's owned by
          if (resource.tableName === `users`) {
            return resource;
          }

          return resource.user().query({}).fetch();
        })
        .then(function(owner) {
          if (!owner) {
            // This should never ever happen
            throw Boom.badImplementation(null, {
              error: `An owning user can't be found (mayday!)`
            });
          }

          return owner;
        })
        .then(function(owner) {
          const ownerId = userIsNotModel ? owner.id : owner.get(`id`);

          if (ownerId !== authenticatedUser.id) {
            throw Boom.unauthorized(null, {
              debug: true,
              error: `User doesn't own the resource requested`
            });
          }
        })
        .catch(Errors.generateErrorResponse);

        return reply(result);
      }
    };
  }

  /**
  * validateExportPermission()
  *
  * Ensures that the resource for which this request was authorized for
  * matches the resource requested.
  *
  * @return {Promise} - Resolved if the authorization matches the requested
  * resource, rejected otherwise.
  */
  static validateExportPermission() {
    return {
      method(request, reply) {
        const requestedId = request.pre.records.models[0].get(`id`);
        const { authorizedId } = request.auth.credentials;

        if (requestedId === authorizedId) {
          return reply(Promise.resolve());
        }

        return reply(Errors.generateErrorResponse(
          Boom.forbidden(
            `Requested resource with id ${requestedId} does not match the resource token provided`
          )
        ));
      }
    };
  }

  /**
  * validateCreationPermission([getUserForModelFn])
  *
  * Ensures the authenticated user is the owner of the
  * resource (or its parent) being created.
  *
  * @param {Function} getUserForModelFn - A function that will fetch a user
  * object as a simple javascript object for the model (or its parent) that
  * this prerequisite function is validating creation permission for.
  *
  * @return {Promise} - Promise fullfilled when the user has been confirmed to
  * be the owner of the resource being created (or its parent)
  */
  static validateCreationPermission(getUserForModelFn) {
    return {
      method(request, reply) {
        const result = Promise.fromCallback(next => {
          return request.server.methods.user(request.auth.credentials.username, next);
        })
        .then(function(user) {
          if (!user) {
            // This case means our auth logic failed unexpectedly
            throw Boom.badImplementation(null, {
              error: `User doesn't exist!`
            });
          }

          // Check to see if there's a direct reference to `user_id` in the payload
          if (!getUserForModelFn) {
            if (user.id !== request.payload.user_id) {
              throw Boom.unauthorized(null, {
                debug: true,
                error: `User doesn't own the resource being referenced`
              });
            }

            return;
          }

          return getUserForModelFn(request)
          .then(function(owner) {
            if (!owner) {
              throw Boom.notFound(null, {
                debug: true,
                error: `Parent foreign key passed in payload (most likely project_id) does not reference an existing model`
              });
            }

            if (user.id !== owner.id) {
              throw Boom.unauthorized(null, {
                debug: true,
                error: `User doesn't own the resource being referenced`
              });
            }
          });
        })
        .catch(Errors.generateErrorResponse);

        reply(result);
      }
    };
  }

  /**
  * getSimpleFileList(model, config)
  *
  * Returns a list of file (or published file) model instances that
  * only have the `id` and `path` for each instance.
  * This method should only be called after the `confirmRecordExists`
  * pre-requisite has been called and the models in the pre-requisite
  * records in the request object has been set.
  *
  * @param {Object} Model - Bookshelf model representing a file for querying
  * @param {Object} config - Contains configuration options for the query:
  *     dbKey {String} - key in the file model being used for filtering
  *     preReqModelKey {String} - key in the pre-requisite model instance to
  *     use for filtering
  *
  * @return {Promise} - Promise fullfilled when the list of files are queried
  * from the database
  */
  static getSimpleFileList(Model, config) {
    const { dbKey, preReqModelKey } = config;

    return {
      assign: `files`,
      method(request, reply) {
        const record = request.pre.records.models[0];

        return reply(
          Model.query({
            where: {
              [dbKey]: record.get(preReqModelKey)
            }
          })
          .fetchAll({ columns: [`id`, `path`] })
        );
      }
    };
  }

  /**
  * trackTemporaryFile()
  *
  * Stores the path to a temporary file in req.app for clearing after a request completes
  * and in req.pre for use in the handler
  *
  * @return {String} - Path to the temporary file
  */
  static trackTemporaryFile() {
    return {
      assign: `tmpFile`,

      /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^reply$" }]*/
      method(request, reply) {
        const buffer = request.payload.buffer;

        // Store the paths for after the request completes
        request.app.tmpFile = buffer.path;

        reply(buffer.path);
      }
    };
  }
}

module.exports = Prerequisites;