Merge remote-tracking branch 'ParsePlatform/master' into user-roles

This commit is contained in:
Francis Lessard
2016-02-12 16:16:20 -05:00
30 changed files with 1047 additions and 409 deletions

View File

@@ -14,6 +14,8 @@
export class FilesAdapter {
createFile(config, filename, data) { }
deleteFile(config, filename) { }
getFileData(config, filename) { }
getFileLocation(config, filename) { }

View File

@@ -20,6 +20,17 @@ export class GridStoreAdapter extends FilesAdapter {
});
}
deleteFile(config, filename) {
return config.database.connect().then(() => {
let gridStore = new GridStore(config.database.db, filename, 'w');
return gridStore.open();
}).then((gridStore) => {
return gridStore.unlink();
}).then((gridStore) => {
return gridStore.close();
});
}
getFileData(config, filename) {
return config.database.connect().then(() => {
return GridStore.exist(config.database.db, filename);

View File

@@ -56,6 +56,20 @@ export class S3Adapter extends FilesAdapter {
});
}
deleteFile(config, filename) {
return new Promise((resolve, reject) => {
let params = {
Key: this._bucketPrefix + filename
};
this._s3Client.deleteObject(params, (err, data) =>{
if(err !== null) {
return reject(err);
}
resolve(data);
});
});
}
// Search for and return a file if found by filename
// Returns a promise that succeeds with the buffer result from S3
getFileData(config, filename) {

View File

@@ -0,0 +1,225 @@
// Logger
//
// Wrapper around Winston logging library with custom query
//
// expected log entry to be in the shape of:
// {"level":"info","message":"{ '0': 'Your Message' }","timestamp":"2016-02-04T05:59:27.412Z"}
//
import { LoggerAdapter } from './LoggerAdapter';
import winston from 'winston';
import fs from 'fs';
import { Parse } from 'parse/node';
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
const CACHE_TIME = 1000 * 60;
let LOGS_FOLDER = './logs/';
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') {
LOGS_FOLDER = './test_logs/'
}
let currentDate = new Date();
let simpleCache = {
timestamp: null,
from: null,
until: null,
order: null,
data: [],
level: 'info',
};
// returns Date object rounded to nearest day
let _getNearestDay = (date) => {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
// returns Date object of previous day
let _getPrevDay = (date) => {
return new Date(date - MILLISECONDS_IN_A_DAY);
}
// returns the iso formatted file name
let _getFileName = () => {
return _getNearestDay(currentDate).toISOString()
}
// check for valid cache when both from and util match.
// cache valid for up to 1 minute
let _hasValidCache = (from, until, level) => {
if (String(from) === String(simpleCache.from) &&
String(until) === String(simpleCache.until) &&
new Date() - simpleCache.timestamp < CACHE_TIME &&
level === simpleCache.level) {
return true;
}
return false;
}
// renews transports to current date
let _renewTransports = ({infoLogger, errorLogger, logsFolder}) => {
if (infoLogger) {
infoLogger.add(winston.transports.File, {
filename: logsFolder + _getFileName() + '.info',
name: 'info-file',
level: 'info'
});
}
if (errorLogger) {
errorLogger.add(winston.transports.File, {
filename: logsFolder + _getFileName() + '.error',
name: 'error-file',
level: 'error'
});
}
};
// check that log entry has valid time stamp based on query
let _isValidLogEntry = (from, until, entry) => {
var _entry = JSON.parse(entry),
timestamp = new Date(_entry.timestamp);
return timestamp >= from && timestamp <= until
? true
: false
};
// ensure that file name is up to date
let _verifyTransports = ({infoLogger, errorLogger, logsFolder}) => {
if (_getNearestDay(currentDate) !== _getNearestDay(new Date())) {
currentDate = new Date();
if (infoLogger) {
infoLogger.remove('info-file');
}
if (errorLogger) {
errorLogger.remove('error-file');
}
_renewTransports({infoLogger, errorLogger, logsFolder});
}
}
export class FileLoggerAdapter extends LoggerAdapter {
constructor(options = {}) {
super();
this._logsFolder = options.logsFolder || LOGS_FOLDER;
// check logs folder exists
if (!fs.existsSync(this._logsFolder)) {
fs.mkdirSync(this._logsFolder);
}
this._errorLogger = new (winston.Logger)({
exitOnError: false,
transports: [
new (winston.transports.File)({
filename: this._logsFolder + _getFileName() + '.error',
name: 'error-file',
level: 'error'
})
]
});
this._infoLogger = new (winston.Logger)({
exitOnError: false,
transports: [
new (winston.transports.File)({
filename: this._logsFolder + _getFileName() + '.info',
name: 'info-file',
level: 'info'
})
]
});
}
info() {
_verifyTransports({infoLogger: this._infoLogger, logsFolder: this._logsFolder});
return this._infoLogger.info.apply(undefined, arguments);
}
error() {
_verifyTransports({errorLogger: this._errorLogger, logsFolder: this._logsFolder});
return this._errorLogger.error.apply(undefined, arguments);
}
// custom query as winston is currently limited
query(options, callback) {
if (!options) {
options = {};
}
// defaults to 7 days prior
let from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY));
let until = options.until || new Date();
let size = options.size || 10;
let order = options.order || 'desc';
let level = options.level || 'info';
let roundedUntil = _getNearestDay(until);
let roundedFrom = _getNearestDay(from);
if (_hasValidCache(roundedFrom, roundedUntil, level)) {
let logs = [];
if (order !== simpleCache.order) {
// reverse order of data
simpleCache.data.forEach((entry) => {
logs.unshift(entry);
});
} else {
logs = simpleCache.data;
}
callback(logs.slice(0, size));
return;
}
let curDate = roundedUntil;
let curSize = 0;
let method = order === 'desc' ? 'push' : 'unshift';
let files = [];
let promises = [];
// current a batch call, all files with valid dates are read
while (curDate >= from) {
files[method](this._logsFolder + curDate.toISOString() + '.' + level);
curDate = _getPrevDay(curDate);
}
// read each file and split based on newline char.
// limitation is message cannot contain newline
// TODO: strip out delimiter from logged message
files.forEach(function(file, i) {
let promise = new Parse.Promise();
fs.readFile(file, 'utf8', function(err, data) {
if (err) {
promise.resolve([]);
} else {
let results = data.split('\n').filter((value) => {
return value.trim() !== '';
});
promise.resolve(results);
}
});
promises[method](promise);
});
Parse.Promise.when(promises).then((results) => {
let logs = [];
results.forEach(function(logEntries, i) {
logEntries.forEach(function(entry) {
if (_isValidLogEntry(from, until, entry)) {
logs[method](JSON.parse(entry));
}
});
});
simpleCache = {
timestamp: new Date(),
from: roundedFrom,
until: roundedUntil,
data: logs,
order,
level,
};
callback(logs.slice(0, size));
});
}
}
export default FileLoggerAdapter;

View File

@@ -0,0 +1,17 @@
// Logger Adapter
//
// Allows you to change the logger mechanism
//
// Adapter classes must implement the following functions:
// * info(obj1 [, obj2, .., objN])
// * error(obj1 [, obj2, .., objN])
// * query(options, callback)
// Default is FileLoggerAdapter.js
export class LoggerAdapter {
info() {}
error() {}
query(options, callback) {}
}
export default LoggerAdapter;

View File

@@ -4,11 +4,9 @@ import express from 'express';
import mime from 'mime';
import { Parse } from 'parse/node';
import BodyParser from 'body-parser';
import hat from 'hat';
import * as Middlewares from '../middlewares';
import Config from '../Config';
const rack = hat.rack();
import { randomHexString } from '../cryptoUtils';
export class FilesController {
constructor(filesAdapter) {
@@ -61,7 +59,7 @@ export class FilesController {
extension = '.' + mime.extension(contentType);
}
let filename = rack() + '_' + req.params.filename + extension;
let filename = randomHexString(32) + '_' + req.params.filename + extension;
this._filesAdapter.createFile(req.config, filename, req.body).then(() => {
res.status(201);
var location = this._filesAdapter.getFileLocation(req.config, filename);
@@ -74,6 +72,19 @@ export class FilesController {
};
}
deleteHandler() {
return (req, res, next) => {
this._filesAdapter.deleteFile(req.config, req.params.filename).then(() => {
res.status(200);
// TODO: return useful JSON here?
res.end();
}).catch((error) => {
next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR,
'Could not delete file.'));
});
};
}
/**
* Find file references in REST-format object and adds the url key
* with the current mount point and app id.
@@ -119,6 +130,13 @@ export class FilesController {
this.createHandler()
);
router.delete('/files/:filename',
Middlewares.allowCrossDomain,
Middlewares.handleParseHeaders,
Middlewares.enforceMasterKeyAccess,
this.deleteHandler()
);
return router;
}
}

View File

@@ -0,0 +1,78 @@
import { Parse } from 'parse/node';
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
const Promise = Parse.Promise;
const INFO = 'info';
const ERROR = 'error';
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
// only allow request with master key
let enforceSecurity = (auth) => {
if (!auth || !auth.isMaster) {
throw new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
'Clients aren\'t allowed to perform the ' +
'get' + ' operation on logs.'
);
}
}
// check that date input is valid
let isValidDateTime = (date) => {
if (!date || isNaN(Number(date))) {
return false;
}
}
export class LoggerController {
constructor(loggerAdapter) {
this._loggerAdapter = loggerAdapter;
}
// Returns a promise for a {response} object.
// query params:
// level (optional) Level of logging you want to query for (info || error)
// from (optional) Start time for the search. Defaults to 1 week ago.
// until (optional) End time for the search. Defaults to current time.
// order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”.
// size (optional) Number of rows returned by search. Defaults to 10
handleGET(req) {
if (!this._loggerAdapter) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Logger adapter is not availabe');
}
let promise = new Parse.Promise();
let from = (isValidDateTime(req.query.from) && new Date(req.query.from)) ||
new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY);
let until = (isValidDateTime(req.query.until) && new Date(req.query.until)) || new Date();
let size = Number(req.query.size) || 10;
let order = req.query.order || 'desc';
let level = req.query.level || INFO;
enforceSecurity(req.auth);
this._loggerAdapter.query({
from,
until,
size,
order,
level,
}, (result) => {
promise.resolve({
response: result
});
});
return promise;
}
getExpressRouter() {
let router = new PromiseRouter();
router.route('GET','/logs', (req) => {
return this.handleGET(req);
});
return router;
}
}
export default LoggerController;

View File

@@ -2,7 +2,7 @@
const Parse = require('parse/node').Parse;
const gcm = require('node-gcm');
const randomstring = require('randomstring');
const cryptoUtils = require('./cryptoUtils');
const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks
const GCMRegistrationTokensMax = 1000;
@@ -22,10 +22,7 @@ function GCM(args) {
* @returns {Object} A promise which is resolved after we get results from gcm
*/
GCM.prototype.send = function(data, devices) {
let pushId = randomstring.generate({
length: 10,
charset: 'alphanumeric'
});
let pushId = cryptoUtils.newObjectId();
let timeStamp = Date.now();
let expirationTime;
// We handle the expiration_time convertion in push.js, so expiration_time is a valid date

View File

@@ -415,6 +415,11 @@ function includePath(config, auth, response, path) {
for (var obj of includeResponse.results) {
obj.__type = 'Object';
obj.className = className;
if(className == "_User"){
delete obj.sessionToken;
}
replace[obj.objectId] = obj;
}
var resp = {

View File

@@ -2,13 +2,12 @@
// that writes to the database.
// This could be either a "create" or an "update".
var crypto = require('crypto');
var deepcopy = require('deepcopy');
var rack = require('hat').rack();
var Auth = require('./Auth');
var cache = require('./cache');
var Config = require('./Config');
var cryptoUtils = require('./cryptoUtils');
var passwordCrypto = require('./password');
var facebook = require('./facebook');
var Parse = require('parse/node');
@@ -57,7 +56,7 @@ function RestWrite(config, auth, className, query, data, originalData) {
this.data.updatedAt = this.updatedAt;
if (!this.query) {
this.data.createdAt = this.updatedAt;
this.data.objectId = newStringId(10);
this.data.objectId = cryptoUtils.newObjectId();
}
}
}
@@ -268,7 +267,7 @@ RestWrite.prototype.handleFacebookAuthData = function() {
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
'this auth is already used');
} else {
this.data.username = rack();
this.data.username = cryptoUtils.newToken();
}
// This FB auth does not already exist, so transform it to a
@@ -289,7 +288,7 @@ RestWrite.prototype.transformUser = function() {
var promise = Promise.resolve();
if (!this.query) {
var token = 'r:' + rack();
var token = 'r:' + cryptoUtils.newToken();
this.storage['token'] = token;
promise = promise.then(() => {
var expiresAt = new Date();
@@ -335,7 +334,7 @@ RestWrite.prototype.transformUser = function() {
// Check for username uniqueness
if (!this.data.username) {
if (!this.query) {
this.data.username = newStringId(25);
this.data.username = cryptoUtils.randomString(25);
}
return;
}
@@ -428,7 +427,7 @@ RestWrite.prototype.handleSession = function() {
}
if (!this.query && !this.auth.isMaster) {
var token = 'r:' + rack();
var token = 'r:' + cryptoUtils.newToken();
var expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var sessionData = {
@@ -721,20 +720,4 @@ RestWrite.prototype.objectId = function() {
return this.data.objectId || this.query.objectId;
};
// Returns a unique string that's usable as an object or other id.
function newStringId(size) {
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'0123456789');
var objectId = '';
var bytes = crypto.randomBytes(size);
for (var i = 0; i < bytes.length; ++i) {
// Note: there is a slight modulo bias, because chars length
// of 62 doesn't divide the number of all bytes (256) evenly.
// It is acceptable for our purposes.
objectId += chars[bytes.readUInt8(i) % chars.length];
}
return objectId;
}
module.exports = RestWrite;

View File

@@ -51,6 +51,11 @@ export class ClassesRouter {
if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
}
if(req.params.className === "_User"){
delete response.results[0].sessionToken;
}
return { response: response.results[0] };
});
}

View File

@@ -51,12 +51,12 @@ export class InstallationsRouter extends ClassesRouter {
}
getExpressRouter() {
var router = new PromiseRouter();
router.route('GET','/installations', (req) => { return this.handleFind(req); });
router.route('GET','/installations/:objectId', (req) => { return this.handleGet(req); });
router.route('POST','/installations', (req) => { return this.handleCreate(req); });
router.route('PUT','/installations/:objectId', (req) => { return this.handleUpdate(req); });
router.route('DELETE','/installations/:objectId', (req) => { return this.handleDelete(req); });
let router = new PromiseRouter();
router.route('GET','/installations', req => { return this.handleFind(req); });
router.route('GET','/installations/:objectId', req => { return this.handleGet(req); });
router.route('POST','/installations', req => { return this.handleCreate(req); });
router.route('PUT','/installations/:objectId', req => { return this.handleUpdate(req); });
router.route('DELETE','/installations/:objectId', req => { return this.handleDelete(req); });
return router;
}
}

View File

@@ -0,0 +1,43 @@
import ClassesRouter from './ClassesRouter';
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
export class RolesRouter extends ClassesRouter {
handleFind(req) {
req.params.className = '_Role';
return super.handleFind(req);
}
handleGet(req) {
req.params.className = '_Role';
return super.handleGet(req);
}
handleCreate(req) {
req.params.className = '_Role';
return super.handleCreate(req);
}
handleUpdate(req) {
req.params.className = '_Role';
return super.handleUpdate(req);
}
handleDelete(req) {
req.params.className = '_Role';
return super.handleDelete(req);
}
getExpressRouter() {
let router = new PromiseRouter();
router.route('GET','/roles', req => { return this.handleFind(req); });
router.route('GET','/roles/:objectId', req => { return this.handleGet(req); });
router.route('POST','/roles', req => { return this.handleCreate(req); });
router.route('PUT','/roles/:objectId', req => { return this.handleUpdate(req); });
router.route('DELETE','/roles/:objectId', req => { return this.handleDelete(req); });
return router;
}
}
export default RolesRouter;

View File

@@ -0,0 +1,63 @@
import ClassesRouter from './ClassesRouter';
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
import Auth from '../Auth';
export class SessionsRouter extends ClassesRouter {
handleFind(req) {
req.params.className = '_Session';
return super.handleFind(req);
}
handleGet(req) {
req.params.className = '_Session';
return super.handleGet(req);
}
handleCreate(req) {
req.params.className = '_Session';
return super.handleCreate(req);
}
handleUpdate(req) {
req.params.className = '_Session';
return super.handleUpdate(req);
}
handleDelete(req) {
req.params.className = '_Session';
return super.handleDelete(req);
}
handleMe(req) {
// TODO: Verify correct behavior
if (!req.info || !req.info.sessionToken) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token required.');
}
return rest.find(req.config, Auth.master(req.config), '_Session', { _session_token: req.info.sessionToken })
.then((response) => {
if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token not found.');
}
return {
response: response.results[0]
};
});
}
getExpressRouter() {
let router = new PromiseRouter();
router.route('GET','/sessions/me', req => { return this.handleMe(req); });
router.route('GET', '/sessions', req => { return this.handleFind(req); });
router.route('GET', '/sessions/:objectId', req => { return this.handleGet(req); });
router.route('POST', '/sessions', req => { return this.handleCreate(req); });
router.route('PUT', '/sessions/:objectId', req => { return this.handleUpdate(req); });
router.route('DELETE', '/sessions/:objectId', req => { return this.handleDelete(req); });
return router;
}
}
export default SessionsRouter;

161
src/Routers/UsersRouter.js Normal file
View File

@@ -0,0 +1,161 @@
// These methods handle the User-related routes.
import deepcopy from 'deepcopy';
import ClassesRouter from './ClassesRouter';
import PromiseRouter from '../PromiseRouter';
import rest from '../rest';
import Auth from '../Auth';
import passwordCrypto from '../password';
import RestWrite from '../RestWrite';
import { newToken } from '../cryptoUtils';
export class UsersRouter extends ClassesRouter {
handleFind(req) {
req.params.className = '_User';
return super.handleFind(req);
}
handleGet(req) {
req.params.className = '_User';
return super.handleGet(req);
}
handleCreate(req) {
let data = deepcopy(req.body);
data.installationId = req.info.installationId;
req.body = data;
req.params.className = '_User';
return super.handleCreate(req);
}
handleUpdate(req) {
req.params.className = '_User';
return super.handleUpdate(req);
}
handleDelete(req) {
req.params.className = '_User';
return super.handleDelete(req);
}
handleMe(req) {
if (!req.info || !req.info.sessionToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.');
}
return rest.find(req.config, Auth.master(req.config), '_Session',
{ _session_token: req.info.sessionToken },
{ include: 'user' })
.then((response) => {
if (!response.results ||
response.results.length == 0 ||
!response.results[0].user) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.');
} else {
let user = response.results[0].user;
return { response: user };
}
});
}
handleLogIn(req) {
// Use query parameters instead if provided in url
if (!req.body.username && req.query.username) {
req.body = req.query;
}
// TODO: use the right error codes / descriptions.
if (!req.body.username) {
throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'username is required.');
}
if (!req.body.password) {
throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'password is required.');
}
let user;
return req.database.find('_User', { username: req.body.username })
.then((results) => {
if (!results.length) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
}
user = results[0];
return passwordCrypto.compare(req.body.password, user.password);
}).then((correct) => {
if (!correct) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.');
}
let token = 'r:' + newToken();
user.sessionToken = token;
delete user.password;
req.config.filesController.expandFilesInObject(req.config, user);
let expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
let sessionData = {
sessionToken: token,
user: {
__type: 'Pointer',
className: '_User',
objectId: user.objectId
},
createdWith: {
'action': 'login',
'authProvider': 'password'
},
restricted: false,
expiresAt: Parse._encode(expiresAt)
};
if (req.info.installationId) {
sessionData.installationId = req.info.installationId
}
let create = new RestWrite(req.config, Auth.master(req.config), '_Session', null, sessionData);
return create.execute();
}).then(() => {
return { response: user };
});
}
handleLogOut(req) {
let success = {response: {}};
if (req.info && req.info.sessionToken) {
return rest.find(req.config, Auth.master(req.config), '_Session',
{ _session_token: req.info.sessionToken }
).then((records) => {
if (records.results && records.results.length) {
return rest.del(req.config, Auth.master(req.config), '_Session',
records.results[0].objectId
).then(() => {
return Promise.resolve(success);
});
}
return Promise.resolve(success);
});
}
return Promise.resolve(success);
}
getExpressRouter() {
let router = new PromiseRouter();
router.route('GET', '/users', req => { return this.handleFind(req); });
router.route('POST', '/users', req => { return this.handleCreate(req); });
router.route('GET', '/users/:objectId', req => { return this.handleGet(req); });
router.route('PUT', '/users/:objectId', req => { return this.handleUpdate(req); });
router.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); });
router.route('GET', '/users/me', req => { return this.handleMe(req); });
router.route('GET', '/login', req => { return this.handleLogIn(req); });
router.route('POST', '/logout', req => { return this.handleLogOut(req); });
router.route('POST', '/requestPasswordReset', () => {
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'This path is not implemented yet.');
});
return router;
}
}
export default UsersRouter;

44
src/cryptoUtils.js Normal file
View File

@@ -0,0 +1,44 @@
import { randomBytes } from 'crypto';
// Returns a new random hex string of the given even size.
export function randomHexString(size) {
if (size === 0) {
throw new Error('Zero-length randomHexString is useless.');
}
if (size % 2 !== 0) {
throw new Error('randomHexString size must be divisible by 2.')
}
return randomBytes(size/2).toString('hex');
}
// Returns a new random alphanumeric string of the given size.
//
// Note: to simplify implementation, the result has slight modulo bias,
// because chars length of 62 doesn't divide the number of all bytes
// (256) evenly. Such bias is acceptable for most cases when the output
// length is long enough and doesn't need to be uniform.
export function randomString(size) {
if (size === 0) {
throw new Error('Zero-length randomString is useless.');
}
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'0123456789');
var objectId = '';
var bytes = randomBytes(size);
for (var i = 0; i < bytes.length; ++i) {
objectId += chars[bytes.readUInt8(i) % chars.length];
}
return objectId;
}
// Returns a new random alphanumeric string suitable for object ID.
export function newObjectId() {
//TODO: increase length to better protect against collisions.
return randomString(10);
}
// Returns a new random hex string suitable for secure tokens.
export function newToken() {
return randomHexString(32);
}

View File

@@ -22,6 +22,7 @@ function handleCloudFunction(req) {
params: req.body || {},
master: req.auth && req.auth.isMaster,
user: req.auth && req.auth.user,
installationId: req.info.installationId
};
Parse.Cloud.Functions[req.params.functionName](request, response);
});

View File

@@ -20,6 +20,12 @@ import { PushController } from './Controllers/PushController';
import { ClassesRouter } from './Routers/ClassesRouter';
import { InstallationsRouter } from './Routers/InstallationsRouter';
import { UsersRouter } from './Routers/UsersRouter';
import { SessionsRouter } from './Routers/SessionsRouter';
import { RolesRouter } from './Routers/RolesRouter';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { LoggerController } from './Controllers/LoggerController';
// Mutate the Parse object to add the Cloud Code handlers
addParseCloud();
@@ -69,6 +75,9 @@ function ParseServer(args) {
pushAdapter = new ParsePushAdapter(pushConfig)
}
// Make logger adapter
let loggerAdapter = args.loggerAdapter || new FileLoggerAdapter();
if (args.databaseURI) {
DatabaseAdapter.setAppDatabaseURI(args.appId, args.databaseURI);
}
@@ -129,14 +138,15 @@ function ParseServer(args) {
let routers = [
new ClassesRouter().getExpressRouter(),
require('./users'),
require('./sessions'),
require('./roles'),
new UsersRouter().getExpressRouter(),
new SessionsRouter().getExpressRouter(),
new RolesRouter().getExpressRouter(),
require('./analytics'),
new InstallationsRouter().getExpressRouter(),
require('./functions'),
require('./schemas'),
new PushController(pushAdapter).getExpressRouter()
new PushController(pushAdapter).getExpressRouter(),
new LoggerController(loggerAdapter).getExpressRouter()
];
if (process.env.PARSE_EXPERIMENTAL_CONFIG_ENABLED || process.env.TESTING) {
routers.push(require('./global_config'));

View File

@@ -178,15 +178,24 @@ var handleParseErrors = function(err, req, res, next) {
}
};
function enforceMasterKeyAccess(req, res, next) {
if (!req.auth.isMaster) {
res.status(403);
res.end('{"error":"unauthorized: master key is required"}');
return;
}
next();
}
function invalidRequest(req, res) {
res.status(403);
res.end('{"error":"unauthorized"}');
}
module.exports = {
allowCrossDomain: allowCrossDomain,
allowMethodOverride: allowMethodOverride,
handleParseErrors: handleParseErrors,
handleParseHeaders: handleParseHeaders
handleParseHeaders: handleParseHeaders,
enforceMasterKeyAccess: enforceMasterKeyAccess
};

View File

@@ -1,48 +0,0 @@
// roles.js
var Parse = require('parse/node').Parse,
PromiseRouter = require('./PromiseRouter'),
rest = require('./rest');
var router = new PromiseRouter();
function handleCreate(req) {
return rest.create(req.config, req.auth,
'_Role', req.body);
}
function handleUpdate(req) {
return rest.update(req.config, req.auth, '_Role',
req.params.objectId, req.body)
.then((response) => {
return {response: response};
});
}
function handleDelete(req) {
return rest.del(req.config, req.auth,
'_Role', req.params.objectId)
.then(() => {
return {response: {}};
});
}
function handleGet(req) {
return rest.find(req.config, req.auth, '_Role',
{objectId: req.params.objectId})
.then((response) => {
if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.');
} else {
return {response: response.results[0]};
}
});
}
router.route('POST','/roles', handleCreate);
router.route('GET','/roles/:objectId', handleGet);
router.route('PUT','/roles/:objectId', handleUpdate);
router.route('DELETE','/roles/:objectId', handleDelete);
module.exports = router;

View File

@@ -1,98 +0,0 @@
// sessions.js
var Auth = require('./Auth'),
Parse = require('parse/node').Parse,
PromiseRouter = require('./PromiseRouter'),
rest = require('./rest');
var router = new PromiseRouter();
function handleCreate(req) {
return rest.create(req.config, req.auth,
'_Session', req.body);
}
function handleUpdate(req) {
return rest.update(req.config, req.auth, '_Session',
req.params.objectId, req.body)
.then((response) => {
return {response: response};
});
}
function handleDelete(req) {
return rest.del(req.config, req.auth,
'_Session', req.params.objectId)
.then(() => {
return {response: {}};
});
}
function handleGet(req) {
return rest.find(req.config, req.auth, '_Session',
{objectId: req.params.objectId})
.then((response) => {
if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.');
} else {
return {response: response.results[0]};
}
});
}
function handleFind(req) {
var options = {};
if (req.body.skip) {
options.skip = Number(req.body.skip);
}
if (req.body.limit) {
options.limit = Number(req.body.limit);
}
if (req.body.order) {
options.order = String(req.body.order);
}
if (req.body.count) {
options.count = true;
}
if (typeof req.body.keys == 'string') {
options.keys = req.body.keys;
}
if (req.body.include) {
options.include = String(req.body.include);
}
return rest.find(req.config, req.auth,
'_Session', req.body.where, options)
.then((response) => {
return {response: response};
});
}
function handleMe(req) {
// TODO: Verify correct behavior
if (!req.info || !req.info.sessionToken) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token required.');
}
return rest.find(req.config, Auth.master(req.config), '_Session',
{ _session_token: req.info.sessionToken})
.then((response) => {
if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN,
'Session token not found.');
}
return {
response: response.results[0]
};
});
}
router.route('POST','/sessions', handleCreate);
router.route('GET','/sessions/me', handleMe);
router.route('GET','/sessions/:objectId', handleGet);
router.route('PUT','/sessions/:objectId', handleUpdate);
router.route('GET','/sessions', handleFind);
router.route('DELETE','/sessions/:objectId', handleDelete);
module.exports = router;

View File

@@ -3,13 +3,13 @@
var express = require('express'),
cache = require('./cache'),
middlewares = require('./middlewares'),
rack = require('hat').rack();
cryptoUtils = require('./cryptoUtils');
var router = express.Router();
// creates a unique app in the cache, with a collection prefix
function createApp(req, res) {
var appId = rack();
var appId = cryptoUtils.randomHexString(32);
cache.apps[appId] = {
'collectionPrefix': appId + '_',
'masterKey': 'master'
@@ -70,4 +70,4 @@ router.post('/rest_configure_app',
module.exports = {
router: router
};
};

View File

@@ -1,212 +0,0 @@
// These methods handle the User-related routes.
var mongodb = require('mongodb');
var Parse = require('parse/node').Parse;
var rack = require('hat').rack();
var Auth = require('./Auth');
var passwordCrypto = require('./password');
var facebook = require('./facebook');
var PromiseRouter = require('./PromiseRouter');
var rest = require('./rest');
var RestWrite = require('./RestWrite');
var deepcopy = require('deepcopy');
var router = new PromiseRouter();
// Returns a promise for a {status, response, location} object.
function handleCreate(req) {
var data = deepcopy(req.body);
data.installationId = req.info.installationId;
return rest.create(req.config, req.auth,
'_User', data);
}
// Returns a promise for a {response} object.
function handleLogIn(req) {
// Use query parameters instead if provided in url
if (!req.body.username && req.query.username) {
req.body = req.query;
}
// TODO: use the right error codes / descriptions.
if (!req.body.username) {
throw new Parse.Error(Parse.Error.USERNAME_MISSING,
'username is required.');
}
if (!req.body.password) {
throw new Parse.Error(Parse.Error.PASSWORD_MISSING,
'password is required.');
}
var user;
return req.database.find('_User', {username: req.body.username})
.then((results) => {
if (!results.length) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Invalid username/password.');
}
user = results[0];
return passwordCrypto.compare(req.body.password, user.password);
}).then((correct) => {
if (!correct) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Invalid username/password.');
}
var token = 'r:' + rack();
user.sessionToken = token;
delete user.password;
req.config.filesController.expandFilesInObject(req.config, user);
var expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
var sessionData = {
sessionToken: token,
user: {
__type: 'Pointer',
className: '_User',
objectId: user.objectId
},
createdWith: {
'action': 'login',
'authProvider': 'password'
},
restricted: false,
expiresAt: Parse._encode(expiresAt)
};
if (req.info.installationId) {
sessionData.installationId = req.info.installationId
}
var create = new RestWrite(req.config, Auth.master(req.config),
'_Session', null, sessionData);
return create.execute();
}).then(() => {
return {response: user};
});
}
// Returns a promise that resolves to a {response} object.
// TODO: share code with ClassesRouter.js
function handleFind(req) {
var options = {};
if (req.body.skip) {
options.skip = Number(req.body.skip);
}
if (req.body.limit) {
options.limit = Number(req.body.limit);
}
if (req.body.order) {
options.order = String(req.body.order);
}
if (req.body.count) {
options.count = true;
}
if (typeof req.body.keys == 'string') {
options.keys = req.body.keys;
}
if (req.body.include) {
options.include = String(req.body.include);
}
if (req.body.redirectClassNameForKey) {
options.redirectClassNameForKey = String(req.body.redirectClassNameForKey);
}
return rest.find(req.config, req.auth,
'_User', req.body.where, options)
.then((response) => {
return {response: response};
});
}
// Returns a promise for a {response} object.
function handleGet(req) {
return rest.find(req.config, req.auth, '_User',
{objectId: req.params.objectId})
.then((response) => {
if (!response.results || response.results.length == 0) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.');
} else {
return {response: response.results[0]};
}
});
}
function handleMe(req) {
if (!req.info || !req.info.sessionToken) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.');
}
return rest.find(req.config, Auth.master(req.config), '_Session',
{_session_token: req.info.sessionToken},
{include: 'user'})
.then((response) => {
if (!response.results || response.results.length == 0 ||
!response.results[0].user) {
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
'Object not found.');
} else {
var user = response.results[0].user;
return {response: user};
}
});
}
function handleDelete(req) {
return rest.del(req.config, req.auth,
req.params.className, req.params.objectId)
.then(() => {
return {response: {}};
});
}
function handleLogOut(req) {
var success = {response: {}};
if (req.info && req.info.sessionToken) {
return rest.find(req.config, Auth.master(req.config), '_Session',
{_session_token: req.info.sessionToken}
).then((records) => {
if (records.results && records.results.length) {
return rest.del(req.config, Auth.master(req.config), '_Session',
records.results[0].objectId
).then(() => {
return Promise.resolve(success);
});
}
return Promise.resolve(success);
});
}
return Promise.resolve(success);
}
function handleUpdate(req) {
return rest.update(req.config, req.auth, '_User',
req.params.objectId, req.body)
.then((response) => {
return {response: response};
});
}
function notImplementedYet(req) {
throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE,
'This path is not implemented yet.');
}
router.route('POST', '/users', handleCreate);
router.route('GET', '/login', handleLogIn);
router.route('POST', '/logout', handleLogOut);
router.route('GET', '/users/me', handleMe);
router.route('GET', '/users/:objectId', handleGet);
router.route('PUT', '/users/:objectId', handleUpdate);
router.route('GET', '/users', handleFind);
router.route('DELETE', '/users/:objectId', handleDelete);
router.route('POST', '/requestPasswordReset', notImplementedYet);
module.exports = router;