Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions core/loadModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Storage from '../lib/Storage.js';
*/
export default async function loadModel(next) {
const modelPath = path.join(this.dir, this.config.modelsDir);
const structure = {};
const schema = {};
let files = {};

/* Load all models in the model directory */
Expand All @@ -41,7 +41,7 @@ export default async function loadModel(next) {

const model = fs.readFileSync(path.join(modelPath, file));

/* Read the model JSON into the structure */
/* Read the model JSON into the schema */
try {
/* Attempt to parse the JSON */
const parsedModel = JSON.parse(model.toString());
Expand All @@ -63,21 +63,14 @@ export default async function loadModel(next) {
}

/* Save */
structure[table] = parsedModel;
schema[table] = parsedModel;
} catch {
throw new SaplingError(`Error parsing model \`${table}\``);
}
}

this.structure = structure;

/* Create a storage instance based on the models */
this.storage = new Storage(this, {
name: this.name,
schema: this.structure,
config: this.config,
dir: this.dir,
});
this.storage = new Storage(this, schema);

if (next) {
next();
Expand Down
11 changes: 5 additions & 6 deletions hooks/sapling/model/retrieve.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,28 @@
/* Dependencies */
import Response from '@sapling/sapling/lib/Response.js';
import SaplingError from '@sapling/sapling/lib/SaplingError.js';
import Utils from '@sapling/sapling/lib/Utils.js';


/* Hook /api/model/:model */
export default async function retrieve(app, request, response) {
if (request.params.model) {
/* Fetch the given model */
const schema = new Utils().deepClone(app.storage.schema[request.params.model] || []);
const rules = app.storage.getRules(request.params.model);

/* If no model, respond with an error */
if (schema.length === 0) {
if (Object.keys(rules).length === 0) {
return new Response(app, request, response, new SaplingError('No such model'));
}

/* Remove any internal/private model values (begin with _) */
for (const k in schema) {
for (const k in rules) {
if (k.startsWith('_')) {
delete schema[k];
delete rules[k];
}
}

/* Send it out */
return new Response(app, request, response, null, schema);
return new Response(app, request, response, null, rules);
}

return new Response(app, request, response, new SaplingError('No model specified'));
Expand Down
5 changes: 4 additions & 1 deletion hooks/sapling/user/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import SaplingError from '@sapling/sapling/lib/SaplingError.js';

/* Hook /api/user/login */
export default async function login(app, request, response) {
/* Fetch the user model */
const rules = app.storage.getRules('users');

/* Find all identifiable fields */
const identifiables = Object.keys(app.storage.schema.users).filter(field => app.storage.schema.users[field].identifiable);
const identifiables = Object.keys(rules).filter(field => rules[field].identifiable);

/* Figure out which request value is used */
let identValue = false;
Expand Down
145 changes: 75 additions & 70 deletions lib/Request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@
import _ from 'underscore';

import { console } from './Cluster.js';
import Response from './Response.js';
import SaplingError from './SaplingError.js';
import Validation from './Validation.js';


Expand Down Expand Up @@ -61,7 +59,7 @@ export default class Request {
getCreatorConstraint(request, role) {
const conditions = {};

if (request.permission && request.permission.role.includes('owner') && role !== 'admin') {
if (request.session && request.session.user && request.permission && request.permission.role.includes('owner') && role !== 'admin') {
conditions._creator = request.session.user._id;
}

Expand All @@ -82,7 +80,7 @@ export default class Request {
/* Request method */
const method = request.method && request.method.toUpperCase();

/* Trim uneeded parts of the request */
/* Trim unneeded parts of the request */
if (parts[0] === '') {
parts.splice(0, 1);
}
Expand Down Expand Up @@ -122,6 +120,71 @@ export default class Request {
}
}

/* Format incoming data */
if (request.body) {
/* Go through every key in incoming data */
for (const key in request.body) {
if (Object.prototype.hasOwnProperty.call(request.body, key)) {
/* Get the corresponding ruleset */
const rule = this.app.storage.getRule(key, collection);

/* Trim incoming data unless otherwise specified in model */
if (typeof request.body[key] === 'string' && (!rule || !('trim' in rule) || rule.trim !== false)) {
request.body[key] = String(request.body[key]).trim();
}

/* If the data is a number, convert from string */
if (rule && rule.type === 'number') {
request.body[key] = Number.parseFloat(request.body[key], 10);
}

/* Ignore CSRF tokens */
if (key === '_csrf') {
delete request.body[key];
}

/* In strict mode, don't allow unknown fields */
if (!rule && this.app.config.strict) {
console.warn('UNKNOWN FIELD', key);
delete request.body[key];
}

/* If this field has no defined access level, we can skip the rest of the checks */
if (!rule || !rule.access) {
continue;
}

/* Get the write access level */
const access = rule.access.w || rule.access;

/* If the field is owner-only, defer to individual op methods to check against it */
if (access === 'owner') {
continue;
}

/* Get the role from session, if any */
const role = this.app.user.getRole({ session: request.session });

/* If we do not have access, raise hell */
if (this.app.user.isRoleAllowed(role, access) === false) {
console.warn(`NO ACCESS TO FIELD '${key}'`);
console.warn(`Current permission level: ${role}`);
console.warn(`Required permission level: ${access}`);
delete request.body[key];
}
}
}

/* Go through every rule */
const rules = this.app.storage.getRules(collection);
for (const key in rules) {
/* If inserting, and a field with a default value is missing, apply default */
if (parts.length <= 2 && !(key in request.body) && 'default' in rules[key]) {
request.body[key] = rules[key].default;
}
}
}

/* Modify the request object */
return _.extend(request, {
collection,
Expand All @@ -140,21 +203,18 @@ export default class Request {
* @param {object} request Request object from Express
* @param {object} response Response object from Express
*/
validateData(request, response) {
const { collection, body, session, type } = request;
validateData(request) {
const { collection, body, type } = request;

/* Get the collection definition */
const rules = this.app.storage.schema[collection] || {};
const rules = this.app.storage.getRules(collection);

let errors = [];
const data = body || {};

/* Get the role from session, if any */
const role = this.app.user.getRole({ session });

/* Model must be defined before pushing data */
if (!rules && this.app.config.strict) {
new Response(this.app, request, response, new SaplingError({
if (Object.keys(rules).length === 0 && this.app.config.strict) {
return [{
status: '500',
code: '1010',
title: 'Non-existent',
Expand All @@ -163,71 +223,25 @@ export default class Request {
type: 'data',
error: 'nonexistent',
},
}));
return false;
}];
}

/* Go through every key in incoming data */
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
/* Ignore CSRF tokens */
if (key === '_csrf') {
delete data[key];
}

/* Get the corresponding ruleset */
const rule = rules[key];

/* Trim incoming data unless otherwise specified in model */
if (typeof data[key] === 'string' && (!rule || !('trim' in rule) || rule.trim !== false)) {
data[key] = String(data[key]).trim();
}
const rule = this.app.storage.getRule(key, collection);

/* If the field isn't defined */
/* If the field isn't defined, skip */
if (!rule) {
/* In strict mode, don't allow unknown fields */
if (this.app.config.strict) {
console.warn('UNKNOWN FIELD', key);
delete data[key];
}

/* Otherwise skip this field */
continue;
}

const dataType = (rule.type || rule).toLowerCase();

/* If the data is a number, convert from string */
if (dataType === 'number') {
data[key] = Number.parseFloat(data[key], 10);
}

/* Test in the validation library */
const error = new Validation().validate(data[key], key, rule);
if (error.length > 0) {
errors = error;
}

/* If this field has no defined access level, we can skip the rest of the checks */
if (!rule.access) {
continue;
}

/* Get the write access level */
const access = rule.access.w || rule.access;

/* If the field is owner-only, defer to individual op methods to check against it */
if (access === 'owner') {
continue;
}

/* If we do not have access, raise hell */
if (this.app.user.isRoleAllowed(role, access) === false) {
console.warn(`NO ACCESS TO FIELD '${key}'`);
console.warn('Current permission level:', role);
console.warn('Required permission level:', access);
delete data[key];
}
}
}

Expand All @@ -240,10 +254,6 @@ export default class Request {
continue;
}

if (typeof rules[key] !== 'object') {
continue;
}

/* We now know the given field does not have a corresponding value
in the incoming data */

Expand All @@ -260,11 +270,6 @@ export default class Request {
},
});
}

/* Set the data to the default value, if provided */
if ('default' in rules[key]) {
data[key] = rules[key].default;
}
}
}

Expand Down
Loading