Internal Validation on Parse Server

Hello everyone,

Before I start working on a PR for a feature I use on my own servers, I was curious to see what you all think of the feasibility.

I was thinking of this post (How to improve default security), and although we have CLPs, ACLs, etc, securing cloud functions is solely up to the developer.

Furthermore, the cloud code guide doesn’t really have anything on validating requests, or the users making them. Even though these are simple, I think it can be overlooked for novice devs.

I personally have a validator function which I pass into the third parameter of Parse.Cloud.define (which I don’t think validator functions are documented either, the SDK says define only has 2 parameters), but I was thinking of how we could make this more readily available.

Proposal:

Global Parse.Cloud.beforeCloud, where you can run any validation before a cloud function is ran, such as making sure a user is logged in,

Or, Parse.Cloud options:

Parse.Cloud.define('hello', () => {
  return 'Hello world!';
}).withOptions({user:true,timeout:500,masterKey:true,revertKey:['a','b','c']});

.withOptions could trigger be a inbuilt validator, which runs based on the users options, such as:

static validateFunction(options,request) {
    if (!options) {
      return true;
    }
    if (options.user && !request.user) {
      throw new Parse.Error(
        Parse.Error.VALIDATION_ERROR,
        'Validation failed. Please login to continue.'
      );
    }
    if (options.master && !request.master) {
      throw new Parse.Error(
        Parse.Error.VALIDATION_ERROR,
        'Validation failed. Master key is required to complete this request.'
      );
    }
    for (const key in options.params) {
      if (request.params[key] === null) {
        throw new Parse.Error(
          Parse.Error.VALIDATION_ERROR,
          `Validation failed. Please specify data for ${key}.`
        );
      }
    }
    if (options.timeout) {
      setTimeout(() => {
        if (!request.complete) {
          throw new Parse.Error(
            Parse.Error.SCRIPT_FAILED,
            `Function failed: "${functionName}. Error: Timeout."`
          );
        }
      },parseFloat(options.timeout) * 100);
    }
    return true;
  }

The inbuilt cloud function options validator could obviously be extended to provide the most benefit to our users.

What do you guys think, worth working on, or should I focus on the docs?

3 Likes

Thanks for suggesting.

What would be the benefit of

Parse.Cloud.define('hello', req => {
  ...
}).withOptions({...});

vs.

Parse.Cloud.define('hello', req => {
  ensureOptions(req, {...});
  ...
});

The benefit would be that the validation would work out of the box, and require very little configuration. To restrict a cloud function to users only, you’d add

.withOptions({user:true})

I know you can do the validation yourself in a seperate function, but I’m thinking about how to provide it out of the box with minimal coding.

It might serve no purpose, but the idea is sorta like inbuilt CLP but for cloud functions, where changing a few parameters on the cloud function changes its security dramatically.

I’m not sure if .withOptions({…}) is the optimal way to do it - I was thinking of passing another parameter after the handler in define but that’s used for applicationId. Basically the idea is extending some simple utility options to help easier validate cloud functions out of the box, without developers needing to code the logic themselves.

I understand, you want to hard code the validation function with predefined options, that wasn’t clear to me.

I like the idea, the question then would be which validation options make sense.

I think it would be readable, but maybe you could easily introduce an additional parameter after the handler, without it being a breaking change.

Correct! And the more developers that add their specific use cases, the more options we could get.

In my initial post I wrote “revertKey” as I see the question “how do I revert a key to it’s original data in a beforeSave without unsetting” often. The idea is providing this parameter to withOptions would do that for you, rather than the req.object.set(key,req.original.get(‘key’)).

Another example could be specifying require params, instead of:

Parse.Cloud.define('hello', req => {
  if (!req.params.data) {
    throw 'Data must be defined';
  }
  if (!req.params.data1) {
    throw 'Data1 must be defined';
  }
});

You’d get:

Parse.Cloud.define('hello', req => {
  // the inbuilt validator has already checked for req.params.data and req.params.data1, safe to proceed
}).withOptions({params:['data','data1']};

maybe

requireMaster // throws if !request.master
requireUser // throws if !request.user
params // loops through parameters and throws if !request.params[key]. can be an array of keys, or object of objects containing type, default, options, and required.
requireKeys // loops through keys and throws if req.object.get(key) is null
timeout // not sure if this serves any benefit
resolveMaster // for beforeSave, beforeFind, etc. If true and req.master, return request.object and don’t run trigger

Data validation usually consists of more than just checking whether a value is set. The value type is usually checked as well, maybe of 2 parameters only one is required to be set, etc.

Without opining on the params option, I think we would look at which options make sense, because an option that is not practical increases the code base to maintain without providing additional value.

I think it’s a useful enhancement with possibility for extension over time, as you pointed out. I would expect to see some feedback on useful options over the next few days.

What options do you think would make sense? Maybe we could provide the inbuilt validation parameters Vue-style

This one doesn’t care for data type:

Parse.Cloud.define('hello', req => {
  // the inbuilt validator has already checked for req.params.data and req.params.data1, safe to proceed
}).withOptions({params:['data','data1']};

This one does:

Parse.Cloud.define('hello', req => {
    // the inbuilt validator has already checked for req.params.data and req.params.data1, safe to proceed
  }).withOptions({params:{
      data: {
        type: String,
        required: true,
      },
      data1 : {
        type: String,
        default: () => ''
     }
}});

I think the data type check and required option would make the option more useful.

Your suggestions all look useful and I think they are already sorted by relevance for most use cases. I think we could wait a day or two for feedback.

Thank you mate. Sorry for getting ahead of myself :joy:. I’m happy to work on a PR when we get some feedback.

Not at all, thanks for your great initiative in suggesting this enhancement!

I’ve been using Parse for 5 years never knew this lol

3 Likes

Same, I only found out when researching for this post. I’ll work on the docs as well, but yeah you can pass a second handler through Parse.Cloud.define.

ParseCloud.define = function (functionName, handler, validationHandler) {
  triggers.addFunction(
    functionName,
    handler,
    validationHandler,
    Parse.applicationId
  );
};
1 Like

For reference, link to related issue.

For anyone that comes across this thread, we’ve merged an internal validator and updated the existing cloud validator. This will be available on the version after 4.3.0, or the current master.

Here are some related docs:

Often, not only is it important that request.params.movie is defined, but also that it is the correct data type. You can do this by providing an Object to the fields parameter in the Validator.

Parse.Cloud.define("averageStars", async (request) => {
  const query = new Parse.Query("Review");
  query.equalTo("movie", request.params.movie);
  const results = await query.find();
  let sum = 0;
  for (let i = 0; i < results.length; ++i) {
    sum += results[i].get("stars");
  }
  return sum / results.length;
},{
  fields : {
    movie : {
      required: true,
      type: String,
      options: val => {
        return val < 20;
      },
      error: "Movie must be less than 20 characters"
    }
  },
  requireUserKeys: {
    accType : {
      options: 'reviewer',
      error: 'Only reviewers can get average stars'
    }
  }
});

This function will only run if:

  • request.params.movie is defined
  • request.params.movie is a String
  • request.params.movie is less than 20 characters
  • request.user is defined
  • request.user.get('accType') is defined
  • request.user.get('accType') is equal to ‘reviewer’

However, the requested user could set ‘accType’ to reviewer, and then recall the function. Here, you could provide validation on a Parse.User beforeSave trigger. beforeSave validators have a few additional options available, to help you make sure your data is secure.

Parse.Cloud.beforeSave(Parse.User, () => {
  // any additional beforeSave logic here
}, {
    fields: {
      accType: {
        default: 'viewer',
        constant: true
      },
    },
});

This means that the field accType on Parse.User will be ‘viewer’ on signup, and will be unchangable, unless masterKey is provided.

The full range of Built-In Validation Options are:

  • requireMaster, whether the function requires a masterKey to run

  • requireUser, whether the function requires a request.user to run

  • validateMasterKey, whether the validator should run on masterKey (defaults to false)

  • fields, an Array or Object of fields that are required on the request.

  • requireUserKeys, an Array of fields to be validated on request.user

The full range of Built-In Validation Options on .fields are:

  • type, the type of the request.params[field] or request.object.get(field)

  • default, what the field should default to if it’s null,

  • required, whether the field is required.

  • options, a singular option, array of options, or custom function of allowed values for the field.

  • constant, whether the field is immutable.

  • error, a custom error message if validation fails.

Massive thanks again to the core team (dplewis and @Manuel especially) for the support in putting this together, and taking the time to go through the PR :pray:.

5 Likes