How can I prevent spoofing of userIDs when creating a document using Meteor and the user-accounts package by verifying the user making the request on the server side?
Here, I add userID to the createdBy field of my workouts entity, but wouldn't a malicious actor be able to choose whatever userID he or she wants?
In lib/collections/workouts.js
Workouts = new Mongo.Collection('workouts');
// Workouts Schema
Workouts.attachSchema(new SimpleSchema({
name: {
type: String,
label: 'Name',
max: 100,
optional: true
},
date: {
type: new Date(),
label: 'Date'
},
feeling: {
type: Number,
label: 'Feeling',
min: 0,
max: 5,
decimal: false
},
notes: {
type: String,
label: 'Notes',
optional: true
},
// Arrays of IDs should be prefixed with a '_'
_sets: {
type: [String],
label: 'Sets',
optional: true
}
}));
// Helpers
Workouts.helpers({
sets: function() {
return Sets.find({ _id: { $in: this._sets } });
}
});
// Hooks
Workouts.before.insert(function(userId, doc) {
doc.createdBy = userId;
});
// Allow server-side publishing
if (Meteor.isServer) {
Workouts.allow({
insert: function (userId, doc) {
return true;
},
update: function (userId, doc, fieldNames, modifier) {
return true;
},
remove: function (userId, doc) {
return true;
}
});
}
In client/templates/workouts/create_workout/create_workout.html
ateWorkout">
<h1>Create Workout</h1>
{{# autoForm collection="Workouts" doc=this id="editWorkoutForm" type="insert"}}
{{> afQuickField name="name"}}
{{> afQuickField name="date"}}
{{> afQuickField name="feeling"}}
{{> afQuickField name="notes" rows=5}}
<button type="create" class="btn btn-primary">Insert</button>
{{/autoForm}}
</template>
I'm using the following packages:
accounts-password 1.1.4 Password support for accounts
aldeed:autoform 5.8.1 Easily create forms with automatic insert ...
aldeed:collection2 2.8.0 Automatic validation of insert and update ...
aldeed:delete-button 2.0.0 Provides a delete button UI component
aldeed:simple-schema 1.5.3 A simple schema validation object with rea...
blaze-html-templates 1.0.1 Compile HTML templates into reactive UI wi...
dburles:collection-helpers 1.0.4 Transform your collections with helpers th...
ecmascript 0.1.6* Compiler plugin that supports ES2015+ in a...
es5-shim 4.1.14 Shims and polyfills to improve ECMAScript...
iron:router 1.0.12 Routing specifically designed for Meteor
jquery 1.11.4 Manipulate the DOM using CSS selectors
matb33:collection-hooks 0.8.1 Extends Mongo.Collection with before/after...
meteor-base 1.0.1 Packages that every Meteor app needs
mobile-experience 1.0.1 Packages for a great mobile user experience
mongo 1.1.3 Adaptor for using MongoDB and Minimongo ov...
session 1.1.1 Session variable
standard-minifiers 1.0.2 Standard minifiers used with Meteor apps b...
tracker 1.0.9 Dependency tracker to allow reactive callb...
twbs:bootstrap 3.3.6 The most popular front-end framework for d...
Instead of using hooks, you can use the autoValue feature of simple-schema. your code snippet will be something like this.
createdBy: {
type: String,
autoValue: function () {
return Meteor.userId();
},
denyUpdate: true,
optional: true
}
Related
Basically... I have something of the following type: record<string, User> (aka Map<String,User>). Basically, an object that can have any field, as long as it is an user.
How do I create an Schema that validates that?
There is an older question that seems to ask the same thing Can SimpleSchema express "object with custom keys and specific schema for values"? , however being asked for an older version of this library.
The accepted answer assumes the usage of Custom Validators
https://github.com/longshotlabs/simpl-schema#type
Another SimpleSchema instance, meaning Object type with this schema
const userSchema = new SimpleSchema({
username: {
type: String,
// For accounts-password, either emails or username is required, but not both. It is OK to make this
// optional here because the accounts-password package does its own validation.
// Third-party login packages may not require either. Adjust this schema as necessary for your usage.
optional: true,
},
emails: {
type: Array,
// For accounts-password, either emails or username is required, but not both. It is OK to make this
// optional here because the accounts-password package does its own validation.
// Third-party login packages may not require either. Adjust this schema as necessary for your usage.
optional: true,
},
'emails.$': {
type: Object,
},
'emails.$.address': {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
'emails.$.verified': {
type: Boolean,
},
// Use this registered_emails field if you are using splendido:meteor-accounts-emails-field / splendido:meteor-accounts-meld
registered_emails: {
type: Array,
optional: true,
},
'registered_emails.$': {
type: Object,
blackbox: true,
},
createdAt: {
type: Date,
},
profile: {
type: Schema.UserProfile,
optional: true,
},
// Make sure this services field is in your schema if you're using any of the accounts packages
services: {
type: Object,
optional: true,
blackbox: true,
},
// DISCLAIMER: This only applies to the first and second version of meteor-roles package.
// https://github.com/Meteor-Community-Packages/meteor-collection2/issues/399
// Add `roles` to your schema if you use the meteor-roles package.
// Option 1: Object type
// If you specify that type as Object, you must also specify the
// `Roles.GLOBAL_GROUP` group whenever you add a user to a role.
// Example:
// Roles.addUsersToRoles(userId, ["admin"], Roles.GLOBAL_GROUP);
// You can't mix and match adding with and without a group since
// you will fail validation in some cases.
roles: {
type: Object,
optional: true,
blackbox: true,
},
// Option 2: [String] type
// If you are sure you will never need to use role groups, then
// you can specify [String] as the type
roles: {
type: Array,
optional: true,
},
'roles.$': {
type: String,
},
// In order to avoid an 'Exception in setInterval callback' from Meteor
heartbeat: {
type: Date,
optional: true,
},
});
const someOtherSchema = new SimpleSchema({
useDocument: {
type: userSchema
},
});
I am trying to use the gridsome-plugin-firestore plugin (https://gridsome.org/plugins/gridsome-source-firestore). I want to use that plugin to connect to a simple firestore database collection called news. News has a number of documents with various fields:
content
published_date
summary
author
title
etc.
Does anyone know how am I supposed to set up the gridsome.config file to access this collection using the gridsome-plugin-firestore plugin?. I cannot figure it out from the instructions given.
The Gridsome docs are a little clearer than npm version, but you need to generate a Firebase Admin SDK private key and download the whole file to your Gridsome app and import it into gridsome.config.js as a module, name it whatever you want for the options > credentials: require field as below.
First, you'll need the Firestore plugin
$ yarn add gridsome-source-firestore
Then in gridsome.config.js
const { db } = require('gridsome-source-firestore')
module.exports = {
plugins: [
{
use: 'gridsome-source-firestore',
options: {
credentials: require('./my-project-firebase-adminsdk-qw2123.json'), //
Replace with your credentials file you downloaded.
debug: true, // Default false, should be true to enable live data updates
ignoreImages: false, // Default false
imageDirectory: 'fg_images', // Default /fg_images
collections: [
{
ref: (db) => {
return db.collection('news')
},
slug: (doc, slugify) => {
return `/news/${slugify(doc.data.title)}`
},
children: [
{
ref: (db, parentDoc) => {
return parentDoc.ref.collection('posts')
},
slug: (doc, slugify) => {
return `/${slugify(doc.data.title)}`
},
}
]
}
]
}
}
]
}
You might have to change "posts" to "content" depending on your DB structure and alter the corresponding page queries to suit, there are some examples and other useful setup info in this Gridsome Firestore starter on Github https://github.com/u12206050/gridsome-firestore-starter.git
Using autoform, it seems the data is being passed from autoform as the Meteor method on my server does get the data, but then doing the database update inside of my method doesn't update my database...what am I missing?
Autoform code...
{{> quickForm collection="Rooms" type="method-update"
doc=this autosave=true id=makeUniqueID
meteormethod="updateRoom"}}
Meteor method:
updateRoom: function (room) {
console.log(room);
Rooms.update({_id: room._id}, { $set: {
checkIn: room.checkIn,
checkOut: room.checkOut,
tenantID: room.tenantID,
available: room.available,
needCleaning: room.needCleaning,
}});
},
My allow/deny rules:
Rooms.allow({
insert() { return false; },
update() { return false; },
remove() { return false; }
});
Rooms.deny({
insert() { return true; },
update() { return true; },
remove() { return true; }
});
Below is what I get from the console log in my from my meteor method. So I do get the changes (in this case change the tenantID and the false to available), but it doesn't update in the database. I'm missing a little detail somewhere but can't see it at this point.
The room variable you are passing to the method is nesting everything under the modifier and $set: keys.
You could simply do:
updateRoom: function (room) {
Rooms.update({_id: room._id}, room.modifier);
},
but that's really insecure because you're passing the whole modifier to the method and a hacker could pass in anything they wanted to.
Better:
updateRoom(room) {
check(room,Object);
check(room._id,String);
{checkIn, checkOut, tenantId, available, needCleaning } = room.modifier.$set;
Rooms.update(room._id, { $set: {checkIn, checkOut, tenantId, available, needCleaning }});
},
I'm getting the error "_id is not allowed by the schema" when trying to use an autoform to update a collection via a ValidatedMethod.
As far as I can see from this example and the official docs there is no expectation for my schema to include the _id field, and I wouldn't expect to be updating the id from an update statement, so I have no idea why this error is happening.
If I switch from using the validated method to writing directly to the collection (with a schema attached to the collection that doesn't have the id in) everything works as expected, so I'm assuming the issue is with my the validate in my ValidatedMethod.
Any idea what I'm doing wrong?
Template: customer-edit.html
<template name="updateCustomerEdit">
{{> quickForm
collection="CustomerCompaniesGlobal"
doc=someDoc
id="updateCustomerEdit"
type="method-update"
meteormethod="CustomerCompanies.methods.update"
singleMethodArgument=true
}}
</template>
Template 'code behind': customer-edit.js
Template.updateCustomerEdit.helpers({
someDoc() {
const customerId = () => FlowRouter.getParam('_id');
const instance = Template.instance();
instance.subscribe('CustomerCompany.get', customerId());
const company = CustomerCompanies.findOne({_id: customerId()});
return company;
}
});
Update Validated Method:
// The update method
update = new ValidatedMethod({
// register the name
name: 'CustomerCompanies.methods.update',
// register a method for validation, what's going on here?
validate: new SimpleSchema({}).validator(),
// the actual database updating part validate has already been run at this point
run( newCustomer) {
console.log("method: update");
return CustomerCompanies.update(newCustomer);
}
});
Schema:
Schemas = {};
Schemas.CustomerCompaniesSchema = new SimpleSchema({
name: {
type: String,
max: 100,
optional: false
},
email: {
type: String,
max: 100,
regEx: SimpleSchema.RegEx.Email,
optional: true
},
postcode: {
type: String,
max: 10,
optional: true
},
createdAt: {
type: Date,
optional: false
}
});
Collection:
class customerCompanyCollection extends Mongo.Collection {};
// Make it available to the rest of the app
CustomerCompanies = new customerCompanyCollection("Companies");
CustomerCompaniesGlobal = CustomerCompanies;
// Deny all client-side updates since we will be using methods to manage this collection
CustomerCompanies.deny({
insert() { return true; },
update() { return true; },
remove() { return true; }
});
// Define the expected Schema for data going into and coming out of the database
//CustomerCompanies.schema = Schemas.CustomerCompaniesSchema
// Bolt that schema onto the collection
CustomerCompanies.attachSchema(Schemas.CustomerCompaniesSchema);
I finally got to the bottom of this. The issue is that autoform passes in a composite object that represents the id of the record to be changed and also a modifier ($set) of the data, rather than just the data itself. So the structure of that object is along the lines of:
_id: '5TTbSkfzawwuHGLhy',
modifier:
{
'$set':
{ name: 'Smiths Fabrication Ltd',
email: 'info#smithsfab.com',
postcode: 'OX10 4RT',
createdAt: Wed Jan 27 2016 00:00:00 GMT+0000 (GMT Standard Time)
}
}
Once I figured that out, I changed my update method to this and everything then worked as expected:
// Autoform specific update method that knows how to unpack the single
// object we get from autoform.
update = new ValidatedMethod({
// register the name
name: 'CustomerCompanies.methods.updateAutoForm',
// register a method for validation.
validate(autoformArgs) {
console.log(autoformArgs);
// Need to tell the schema that we are passing in a mongo modifier rather than just the data.
Schemas.CustomerCompaniesSchema.validate(autoformArgs.modifier , {modifier: true});
},
// the actual database updating part
// validate has already been run at this point
run(autoformArgs)
{
return CustomerCompanies.update(autoformArgs._id, autoformArgs.modifier);
}
});
Excellent. Your post helped me out when I was struggling to find any other information on the topic.
To build on your answer, if for some reason you want to get the form data as a single block you can use the following in AutoForm.
type="method" meteormethod="myValidatedMethodName"
Your validated method then might look something like this:
export const myValidatedMethodName = new ValidatedMethod({
name: 'Users.methods.create',
validate(insertDoc) {
Schemas.NewUser.validate(insertDoc);
},
run(insertDoc) {
return Collections.Users.createUser(insertDoc);
}
});
NB: The Schema.validate() method then requires an Object, not the modifier as before.
I'm unclear if there are any clear advantages to either method in general.
The type="method-update" is obviously the way you want to go for updating documents because you get the modifier. The type="method" seems to be the best way to go for creating a new document. It would likely also be the best option in most cases where you're not intending to create a document from the form data.
I'm attempting to use the bozhao:link-accounts package from Atmosphere, but with no luck. Rather than linking the accounts, it is creating additional accounts. Here is what my implementation looks like:
PACKAGES ADDED:
meteor add accounts-facebook
meteor add accounts-twitter
meteor add bozhao:link-accounts
meteor add service-configuration
<!--/client/signIn.html-->
<template name="signInTmpl">
<button class="btn btn-facebook">Sign in with Facebook</button>
</template>
<!--/client/linkTweets.html-->
<template name="linkTweetsTmpl">
<button class="btn btn-twitter">Connect Twitter Account</button>
</template>
//client/signIn.js
Template.siginInTmpl.events({
'click .btn-facebook': function() {
Meteor.loginWithFacebook({requestPermissions: ['email']}, function(err) {
//Do if conditional ...
})
}
})
//client/linkTweets.js
Template.linkTweetsTmpl.events({
'click .btn-twitter': function() {
Meteor.linkWithTwitter(function(err) {
//Do if conditional ...
})
}
})
//server/accounts.js
var createServiceConfiguration;
createServiceConfiguration = function(service, clientId, secret) {
var config;
ServiceConfiguration.configurations.remove({
service: service
});
config = {
generic: {
service: service,
clientId: clientId,
secret: secret
},
instagram: {
service: service,
clientId: clientId,
scope: 'basic',
secret: secret,
loginStyle: "redirect"
},
facebook: {
service: service,
appId: clientId,
secret: secret,
loginStyle: "redirect"
},
twitter: {
service: service,
consumerKey: clientId,
secret: secret,
loginStyle: "redirect"
}
};
switch (service) {
case 'instagram':
return ServiceConfiguration.configurations.insert(config.instagram);
case 'facebook':
return ServiceConfiguration.configurations.insert(config.facebook);
case 'twitter':
return ServiceConfiguration.configurations.insert(config.twitter);
default:
return ServiceConfiguration.configurations.insert(config.generic);
}
};
//DEV KEYS
createServiceConfiguration('instagram', 'api-key', 'api-secret')
createServiceConfiguration('facebook', 'api-key', 'api-secret')
createServiceConfiguration('twitter', 'api-key', 'api-secret')
createServiceConfiguration('google', 'api-key', 'api-secret')
I successfully sign in with Facebook and when I click on the "Connect Twitter Button" it goes through the authorisation sequence fine, but then creates another account altogether.
I recommend using https://atmospherejs.com/splendido/accounts-meld instead to do what you want. The documentation is pretty self explanatory and it supports a wide range of account melding configurations.