├── .gitignore ├── .travis.yml ├── Makefile ├── package.json ├── test ├── validation.js ├── crud.js └── access.js ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage.html 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | 6 | services: 7 | - mongodb -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @node node_modules/lab/bin/lab 3 | test-cov: 4 | @node node_modules/lab/bin/lab -t 100 5 | test-lcov: 6 | @node node_modules/lab/bin/lab -t 100 -r lcov | ./node_modules/coveralls/bin/coveralls.js 7 | test-cov-html: 8 | @node node_modules/lab/bin/lab -r html -o coverage.html 9 | .PHONY: test test-cov test-cov-html 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toothache", 3 | "version": "2.0.0", 4 | "description": "Hapi plugin that removes the toothache from creating CRUD endpoints for MongoDB.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test-lcov" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/smaxwellstewart/toothache" 12 | }, 13 | "keywords": [ 14 | "Hapi", 15 | "Mongo", 16 | "MongoDB", 17 | "CRUD", 18 | "RESTful", 19 | "RESOURCEful", 20 | "toothache" 21 | ], 22 | "author": "Simon Maxwell-Stewart (http://smaxwellstewart.com/)", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/smaxwellstewart/toothache/issues" 26 | }, 27 | "homepage": "https://github.com/smaxwellstewart/toothache", 28 | "dependencies": { 29 | "bcryptjs": "^2.0.2", 30 | "boom": "^2.5.1", 31 | "extend": "^1.3.0", 32 | "joi": "^4.6.2", 33 | "kerberos": "0.0.17", 34 | "mongodb": "^2.0.48" 35 | }, 36 | "devDependencies": { 37 | "coveralls": "^2.11.1", 38 | "hapi": "^6.7.1", 39 | "hapi-auth-hawk": "^1.1.1", 40 | "hawk": "^2.2.3", 41 | "lab": "^4.2.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/validation.js: -------------------------------------------------------------------------------- 1 | var Lab = require("lab"), 2 | Hapi = require("hapi"), 3 | Joi = require("joi"), 4 | MongoDB = require('mongodb').Db, 5 | Server = require('mongodb').Server, 6 | ObjectId = require('mongodb').ObjectID, 7 | Bcrypt = require('bcryptjs'); 8 | 9 | // Internal config stuff 10 | var CRUD = { 11 | collection: 'resources3', 12 | create: { 13 | bcrypt: 'password', 14 | date: 'created', 15 | payload: Joi.object().keys({ 16 | email: Joi.string().required(), 17 | password: Joi.string().required() 18 | }), 19 | defaults: { 20 | access: 'normal', 21 | activated: false 22 | }, 23 | }, 24 | update: { 25 | bcrypt: 'password', 26 | date: 'updated', 27 | payload: Joi.object().keys({ 28 | email: Joi.string(), 29 | password: Joi.string() 30 | }) 31 | }, 32 | validationOpts: { 33 | abortEarly: false 34 | } 35 | }; 36 | 37 | 38 | 39 | // Test shortcuts 40 | var lab = exports.lab = Lab.script(); 41 | var before = lab.before; 42 | var after = lab.after; 43 | var describe = lab.describe; 44 | var it = lab.it; 45 | var expect = Lab.expect; 46 | 47 | describe("Toothache", function() { 48 | 49 | var server = new Hapi.Server(); 50 | 51 | before(function (done) { 52 | var MongoClient = require('mongodb').MongoClient 53 | MongoClient.connect('mongodb://127.0.0.1:27017/test', function(err, db) { 54 | expect(err).to.not.exist; 55 | 56 | // Construct Resource CRUD 57 | CRUD.db = db; 58 | var Resource = require('../')(CRUD); 59 | 60 | // Get All 61 | server.route({ 62 | method: 'GET', path: '/api/resource', 63 | config: { 64 | handler: Resource.find 65 | } 66 | }); 67 | 68 | // Create 69 | server.route({ 70 | method: 'POST', path: '/api/resource', 71 | config: { 72 | handler: Resource.create 73 | } 74 | }); 75 | 76 | // Get a resource 77 | server.route({ 78 | method: 'GET', path: '/api/resource/{id}', 79 | config: { 80 | handler: Resource.get 81 | } 82 | }); 83 | 84 | // Update 85 | server.route({ 86 | method: 'PUT', path: '/api/resource/{id}', 87 | config: { 88 | handler: Resource.update 89 | } 90 | }); 91 | 92 | // Delete 93 | server.route({ 94 | method: 'DELETE', path: '/api/resource/{id}', 95 | config: { 96 | handler: Resource.del 97 | } 98 | }); 99 | done(); 100 | 101 | }) 102 | }); // Done with before 103 | 104 | 105 | it("create route gives error if invalid payload", function(done) { 106 | 107 | var badPayload = { 108 | random: "junk@test.com" 109 | }; 110 | 111 | var options = { 112 | method: "POST", 113 | url: "/api/resource", 114 | payload: JSON.stringify(badPayload) 115 | }; 116 | server.inject(options, function(response) { 117 | var result = response.result; 118 | 119 | expect(response.statusCode).to.equal(400); 120 | expect(result).to.be.instanceof(Object); 121 | expect(result.error).to.equal('Bad Request'); 122 | 123 | done(); 124 | }); 125 | }); 126 | 127 | it("update route gives error if invalid payload", function(done) { 128 | 129 | var payload = { 130 | email: "test@test.com", 131 | password: "newpass" 132 | }; 133 | 134 | var options = { 135 | method: "POST", 136 | url: "/api/resource", 137 | payload: JSON.stringify(payload) 138 | }; 139 | // first create a resource 140 | server.inject(options, function(response) { 141 | var result = response.result; 142 | 143 | var badPayload = { 144 | random: "junk@test.com" 145 | }; 146 | var options = { 147 | method: "PUT", 148 | url: "/api/resource/"+result._id, 149 | payload: JSON.stringify(badPayload) 150 | }; 151 | 152 | server.inject(options, function(response) { 153 | var result = response.result; 154 | 155 | expect(response.statusCode).to.equal(400); 156 | expect(result).to.be.instanceof(Object); 157 | expect(result.error).to.equal('Bad Request'); 158 | 159 | options.method = "DELETE"; 160 | 161 | server.inject(options, function(response) { 162 | 163 | }) 164 | 165 | 166 | done(); 167 | }); 168 | }); 169 | }); 170 | 171 | it("get route gives errors if can't find doc", function(done) { 172 | 173 | 174 | var options = { 175 | method: "GET", 176 | url: "/api/resource/0000000000d0a1b87bfb0683" 177 | }; 178 | server.inject(options, function(response) { 179 | var result = response.result; 180 | 181 | expect(response.statusCode).to.equal(400); 182 | expect(result).to.be.instanceof(Object); 183 | expect(result.error).to.equal('Bad Request'); 184 | expect(result.message).to.equal('No doc found in resources3'); 185 | 186 | done(); 187 | }); 188 | }); 189 | 190 | it("update route gives errors if can't find doc", function(done) { 191 | var payload = { 192 | email: "junk@test.com" 193 | }; 194 | var options = { 195 | method: "PUT", 196 | url: "/api/resource/0000000000d0a1b87bfb0683", 197 | payload: JSON.stringify(payload) 198 | }; 199 | server.inject(options, function(response) { 200 | var result = response.result; 201 | 202 | expect(response.statusCode).to.equal(400); 203 | expect(result).to.be.instanceof(Object); 204 | expect(result.error).to.equal('Bad Request'); 205 | expect(result.message).to.equal('No doc found in resources3'); 206 | 207 | done(); 208 | }); 209 | }); 210 | 211 | it("get route gives errors if can't find doc", function(done) { 212 | var options = { 213 | method: "DELETE", 214 | url: "/api/resource/0000000000d0a1b87bfb0683" 215 | }; 216 | server.inject(options, function(response) { 217 | var result = response.result; 218 | 219 | expect(response.statusCode).to.equal(400); 220 | expect(result).to.be.instanceof(Object); 221 | expect(result.error).to.equal('Bad Request'); 222 | expect(result.message).to.equal('No doc found in resources3'); 223 | 224 | done(); 225 | }); 226 | }); 227 | 228 | 229 | 230 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Toothache 2 | --------- 3 | 4 | A Hapi plugin that removes the toothache from creating CRUD endpoints for MongoDB. 5 | 6 | Current version: **2.0.x** [![Build Status](https://travis-ci.org/smaxwellstewart/toothache.svg?branch=master)](https://travis-ci.org/smaxwellstewart/toothache) [![Coverage Status](https://img.shields.io/coveralls/smaxwellstewart/toothache.svg)](https://coveralls.io/r/smaxwellstewart/toothache?branch=master) 7 | 8 | ## What is this plugin? 9 | 10 | This plugin instantly adds the following functionality to any mongo db... 11 | 12 | * Plug 'n' play CRUD Routes 13 | * Set custom fields to bcrypt and/or timestamp at doc creation, if required 14 | * Access control of resources. 15 | 16 | ## Usage 17 | 18 | The below is intended to be added into a hapi plugin. In our example case, we will make a `User` endpoint for a Hapi server. 19 | 20 | ### Configure 21 | 22 | Configure toothache with desired behaviour... 23 | 24 | ```js 25 | // User model 26 | var CRUD = { 27 | db: db, // MongoDB connection 28 | collection: 'users', // MongoDB collection 29 | // Create options 30 | create: { 31 | // Valid create payload 32 | payload: Joi.object().keys({ 33 | email: Joi.string().required(), 34 | password: Joi.string().required() 35 | }), 36 | defaults: { // Default values that will be added at doc creation 37 | access: 'normal', 38 | activated: false, 39 | uId: true // Field used for access control. This is a special field that when set to true will default to user's id 40 | // The value comes from, 'request.auth.artifacts.id' ie the id the user authenticates with 41 | }, 42 | bcrypt: 'password', // Sets 'password' field to be bcrypted at doc creation 43 | date: 'created', // Sets 'created' field to be dated at doc creation 44 | access: "admin" // Sets which role can create 45 | }, 46 | // Read options for get and find 47 | read: { 48 | whitelist: ['email'], // Array of fields that will be returned, all other fields will be excluded 49 | blacklist: ['password'], // Array of fields that will be removed, all other fields will be included 50 | access: 'normal' // Sets which role can read 51 | }, 52 | // Update options 53 | update: { 54 | // Valid update payload 55 | payload: Joi.object().keys({ 56 | email: Joi.string(), 57 | password: Joi.string() 58 | }), 59 | bcrypt: 'password', // Sets 'password' field to be bcrypted at doc update 60 | date: 'updated', // Sets 'updated' field to be dated at doc update 61 | access: 'normal' // Sets which role can update 62 | }, 63 | // Delete options 64 | del: { 65 | access: 'normal' // Sets which role can update 66 | }, 67 | // Joi options when validating payloads 68 | validationOpts: { 69 | abortEarly: false 70 | } 71 | 72 | }; 73 | 74 | var User = require('toothache')(CRUD); 75 | ``` 76 | 77 | ### Request Handlers 78 | 79 | Once we have configured toothache, the following request handlers will be exposed: 80 | 81 | #### `.create` 82 | - This handler will insert any supplied `payload` into MongoDB. 83 | - Accepted methods: `GET` with `payload` in URL or, `POST` or `PUT` with `payload` in request body. 84 | - The following toothache `options` will affect this handler: 85 | - `db` - MongoDB connection object, connection [example](https://gist.github.com/smaxwellstewart/9cf26df20cb58a3f5d02). 86 | - 'collection' - the MongoDB collection to create, read, update and delete from. 87 | - `create.payload` - [Joi](https://github.com/hapijs/joi) object payload is validated against. 88 | - `create.defaults` - Object of default fields, the payload will extend this object before insertion, 89 | e.g. supplied payload will join and override this default object. 90 | - `create.bcrypt` - Field name of `payload` field to be bcrypted before doc creation. 91 | - `create.date` - Will add a javasctipt `new Date()` timestamp to field name at doc creation. 92 | - `create.access` - If set to `admin` only admin users will be able to create a doc. If set to normal, both admin and normal users have create access. 93 | 94 | #### `.get` 95 | - This handler will return an individual MongoDB document. 96 | - Accepted methods: `GET` with an `id` parameter set in route's `path` field. 97 | - The following toothache `options` will affect this handler: 98 | - `read.whitelist` - Array of fields that will be returned when doc is fetched. 99 | - `read.blacklist` - Array of fields that will be excluded when doc is fetched. Not recommened to be set with `read.whitelist`. 100 | - `read.access` - If set to `admin` only admin users will be able to read a doc. If set to normal, both admin and normal users have read access. 101 | 102 | #### `.find` 103 | - This handler will return an array of MongoDB documents. The search will query with a supplied `payload`, if none is supplied will return all docs. For normal users 104 | - Accepted methods: `GET` with `payload` in URL or, `POST` or `PUT` with `payload` in request body. 105 | - The following toothache `options` will affect this handler: 106 | - `read.whitelist` - Array of fields that will be returned when docs are fetched. 107 | - `read.blacklist` - Array of fields that will be excluded when docs are fetched. Not recommened to be set with `read.whitelist`. 108 | - `read.access` - If set to `admin` only admin users will be able to read a doc. If set to normal, both admin and normal users have read access. 109 | 110 | #### `.update` 111 | - This route will update a doc with any supplied `payload`. The handler expects an `id` parameter to be set in route's `path` field. 112 | - Accepted methods: `GET` with `payload` in URL or, `POST` or `PUT` with `payload` in request body. 113 | - The following toothache `options` will affect this handler: 114 | - `update.payload` - [Joi](https://github.com/hapijs/joi) object payload is validated against. 115 | e.g. supplied payload will join and override this default object. 116 | - `update.bcrypt` - Field name of `payload` field to be bcrypted when doc is updated. 117 | - `update.date` - Will add a javasctipt `new Date()` timestamp to field name when doc is updated. 118 | - `update.access` - If set to `admin` only admin users will be able to update a doc. If set to normal, both admin and normal users have update access. 119 | 120 | #### `.del` 121 | - This route will delete a doc with any supplied `payload`. 122 | - Accepted methods: `DELETE` with an `id` parameter set in route's `path` field. 123 | - The following toothache `options` will affect this handler: 124 | - `del.access` - If set to `admin` only admin users will be able to delete a doc. If set to normal, both admin and normal users have delete access. 125 | 126 | *Example* 127 | 128 | These can be used in a Hapi plugin like this... 129 | 130 | ```js 131 | // Create 132 | plugin.route({ 133 | method: 'POST', path: '/user', 134 | config: { 135 | handler: User.create 136 | } 137 | }); 138 | 139 | // Get a resource, must use 'id' parameter to refer to mongo's '_id' field 140 | plugin.route({ 141 | method: 'GET', path: '/user/{id}', 142 | config: { 143 | handler: User.get 144 | } 145 | }); 146 | 147 | // Get All 148 | plugin.route({ 149 | method: 'GET', path: '/user', 150 | config: { 151 | handler: User.find 152 | } 153 | }); 154 | 155 | // Find, will search collection using payload for criteria 156 | plugin.route({ 157 | method: 'POST', path: '/user/find', 158 | config: { 159 | handler: User.find 160 | } 161 | }); 162 | 163 | // Update, must use 'id' parameter to refer to mongo's '_id' field 164 | plugin.route({ 165 | method: 'PUT', path: '/user/{id}', 166 | config: { 167 | handler: User.update 168 | } 169 | }); 170 | 171 | // Delete, must use 'id' parameter to refer to mongo's '_id' field 172 | plugin.route({ 173 | method: 'DELETE', path: '/user/{id}', 174 | config: { 175 | handler: User.del 176 | } 177 | }); 178 | ``` 179 | 180 | ### Access Control 181 | 182 | #### Roles 183 | - `admin` 184 | - `normal` 185 | 186 | 187 | Access control is only added if a route is authenticated. An `access` field must be added to user's credentials at authentication. For example: 188 | 189 | ```js 190 | // Example: Hawk Auth Lookup 191 | getCredentialsFunc: function (id, callback) { 192 | var credentials = { 193 | user1: { 194 | key: 'pass1', 195 | access: 'admin', 196 | algorithm: 'sha256' 197 | }, 198 | user2: { 199 | key: 'pass2', 200 | access: 'normal', 201 | algorithm: 'sha256' 202 | } 203 | } 204 | return callback(null, credentials[id]); 205 | } 206 | ``` 207 | 208 | - Admin users get access to all resources, they can create, read, update and delete. 209 | - Normal users only have access to their own resources, they can only CRUD documents that have a `uId` equal to user's authenitcation id (`request.auth.artifacts.id`) 210 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by kidtronnix on 20/05/14. 3 | */ 4 | var Boom = require('boom'); 5 | var Joi = require('joi'); 6 | var MongoDB = require('mongodb').Db; 7 | var Server = require('mongodb').Server; 8 | var ObjectId = require('mongodb').ObjectID; 9 | var Extend = require('extend'); 10 | var Bcrypt = require('bcryptjs'); 11 | var salt = Bcrypt.genSaltSync(10); 12 | 13 | 14 | 15 | module.exports = function(config) { 16 | 17 | var baseConfig = { 18 | create: { 19 | access: 'normal' 20 | }, 21 | read: { 22 | access: 'normal' 23 | }, 24 | update: { 25 | access: 'normal' 26 | }, 27 | del: { 28 | access: 'normal' 29 | } 30 | }; 31 | 32 | config = Extend({},baseConfig,config); 33 | 34 | 35 | // get db from config 36 | var db = config.db; 37 | // get mongo collection 38 | var coll = config.collection; 39 | 40 | var CRUD = { 41 | create: function(request, reply) { 42 | 43 | 44 | // add access control 45 | // We need to stop create if not allowed, only if: 46 | // route is authenticated, we are not admin, and we protect create 47 | if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin' && request.auth.credentials.access !== config.create.access) { 48 | var error = Boom.unauthorized('You do not have create access'); 49 | return reply(error); 50 | } 51 | else { 52 | var validSchema = config.create.payload; 53 | 54 | if(request.method === 'get') { 55 | var payload = request.query; 56 | } else { 57 | var payload = request.payload; 58 | } 59 | 60 | // First validate schema 61 | // respond with errors 62 | Joi.validate(payload, validSchema, config.validationOpts, function (err, value) { 63 | if(err) { 64 | var error = Boom.badRequest(err); 65 | return reply(error); 66 | } 67 | else { 68 | 69 | // Add our defaults 70 | var insert = Extend({},config.create.defaults, payload); 71 | 72 | // If config has date option, add a timestamp 73 | if(config.create.date) { 74 | var ts = new Date(); 75 | insert[config.create.date] = ts; 76 | } 77 | 78 | if(config.create.bcrypt) { 79 | // Hash password before insert 80 | insert[config.create.bcrypt] = Bcrypt.hashSync(insert[config.create.bcrypt], salt); 81 | } 82 | 83 | 84 | // Add uId if set to anything in defaults 85 | if(request.auth.isAuthenticated && config.create.defaults["uId"] !== undefined) { 86 | insert.uId = request.auth.artifacts.id; 87 | } 88 | 89 | // Connect to mongo 90 | var collection = db.collection(coll); 91 | 92 | // Perform Insert 93 | collection.insert(insert, function(err, docs) { 94 | 95 | return reply(docs.ops[0]).type('application/json'); 96 | }); 97 | } 98 | }); 99 | } 100 | }, 101 | get: function(request, reply) { 102 | 103 | if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin' && request.auth.credentials.access !== config.read.access) { 104 | var error = Boom.unauthorized('You do not have read access'); 105 | return reply(error); 106 | } 107 | else { 108 | var collection = db 109 | .collection(coll) 110 | .findOne({"_id": ObjectId(request.params.id)}, function(err, doc) { 111 | 112 | 113 | if(doc == null) { 114 | var error = Boom.badRequest('No doc found in '+coll); 115 | return reply(error); 116 | } 117 | // access control 118 | else if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin' && doc.uId !== request.auth.artifacts.id) { 119 | var error = Boom.unauthorized('You are not permitted to see this'); 120 | return reply(error); 121 | } 122 | else { 123 | 124 | if(config.read.whitelist || config.read.blacklist) { 125 | 126 | if(config.read.whitelist) { 127 | // Add whitelist fields 128 | var _doc = {}; 129 | for(var i = 0; i < config.read.whitelist.length; i++) { 130 | var key = config.read.whitelist[i]; 131 | if(doc[key] !== undefined) { 132 | _doc[key] = doc[key]; 133 | } 134 | } 135 | 136 | doc = _doc; 137 | } 138 | if(config.read.blacklist) { 139 | // Remove blacklist fields 140 | for(var i = 0; i < config.read.blacklist.length; i++) { 141 | var key = config.read.blacklist[i]; 142 | delete doc[key]; 143 | } 144 | } 145 | } 146 | return reply(doc).type('application/json'); 147 | } 148 | }); 149 | } 150 | 151 | 152 | }, 153 | find: function(request, reply) { 154 | // console.log(config.read.access) 155 | if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin' && request.auth.credentials.access !== config.read.access) { 156 | var error = Boom.unauthorized('You do not have read access'); 157 | return reply(error); 158 | } 159 | else { 160 | var find = {}; 161 | 162 | if(request.method === 'get') { 163 | var payload = request.query; 164 | } else { 165 | var payload = request.payload; 166 | } 167 | 168 | // Add payload to find object 169 | if(request.payload) { 170 | Extend(find, payload); 171 | } 172 | 173 | // Access Control 174 | if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin') { 175 | var uId = request.auth.artifacts.id; 176 | find.uId = uId; 177 | } 178 | 179 | 180 | 181 | var collection = db 182 | .collection(coll) 183 | .find(find) 184 | .sort({ "_id" : 1}) 185 | .toArray(function(err, docs) { 186 | 187 | var _docs = []; 188 | if(config.read.whitelist || config.read.blacklist) { 189 | 190 | for(var i = 0; i < docs.length; i++) { 191 | var doc = docs[i]; 192 | 193 | if(config.read.whitelist) { 194 | // Add whitelist fields 195 | var _doc = {} 196 | 197 | for(var j = 0; j < config.read.whitelist.length; j++) { 198 | var key = config.read.whitelist[j]; 199 | if(doc[key] !== undefined) { 200 | _doc[key] = doc[key]; 201 | } 202 | 203 | 204 | } 205 | _docs.push(_doc) 206 | } 207 | if(config.read.blacklist) { 208 | // Remove blacklist fields 209 | 210 | //console.log(config.read.blacklist.length) 211 | for(var j = 0; j < config.read.blacklist.length; j++) { 212 | var key = config.read.blacklist[j]; 213 | 214 | delete doc[key]; 215 | } 216 | _docs.push(doc) 217 | } 218 | } 219 | docs = _docs; 220 | 221 | } 222 | 223 | return reply(docs).type('application/json'); 224 | }); 225 | } 226 | 227 | 228 | 229 | }, 230 | update: function(request, reply) { 231 | // console.log(config.update.access) 232 | if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin' && request.auth.credentials.access !== config.update.access) { 233 | var error = Boom.unauthorized('You do not have update access'); 234 | 235 | return reply(error); 236 | } 237 | else { 238 | // Resource ID from URL 239 | var resourceId = request.params.id; 240 | var validSchema = config.update.payload; 241 | 242 | if(request.method === 'get') { 243 | var payload = request.query; 244 | } else { 245 | var payload = request.payload; 246 | } 247 | 248 | Joi.validate(payload, validSchema, config.validationOpts, function (err, value) { 249 | if(err !== null) { 250 | var error = Boom.badRequest(err); 251 | return reply(error); 252 | } 253 | else { 254 | var update = payload; 255 | 256 | if(config.update.bcrypt && update[config.update.bcrypt]) { 257 | // Hash password before update 258 | update[config.update.bcrypt] = Bcrypt.hashSync(update[config.update.bcrypt], salt); 259 | } 260 | if(config.update.date) { 261 | var ts = new Date(); 262 | update[config.update.date] = ts; 263 | } 264 | 265 | // Update Resource with payload 266 | var collection = db.collection(coll); 267 | 268 | // Check doc exists & uId matches doc 269 | collection.findOne({"_id": ObjectId(request.params.id)}, function(err, doc) { 270 | 271 | // doc exists 272 | if(doc === null) { 273 | var error = Boom.badRequest('No doc found in '+coll); 274 | return reply(error); 275 | } 276 | // access control 277 | else if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin' && doc.uId !== request.auth.artifacts.id) { 278 | var error = Boom.unauthorized('You are not permitted to update this'); 279 | return reply(error); 280 | } 281 | else { 282 | collection.update({"_id": ObjectId(resourceId)}, {$set: update}, {}, function(err, doc) { 283 | 284 | return reply({error:null,message:'Updated successfully'}); 285 | }); 286 | } 287 | }) 288 | } 289 | }); 290 | } 291 | 292 | }, 293 | del: function(request, reply) { 294 | if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin' && request.auth.credentials.access !== config.del.access) { 295 | var error = Boom.unauthorized('You do not have delete access'); 296 | return reply(error); 297 | } else { 298 | var _del = {"_id": ObjectId(request.params.id)}; 299 | 300 | var collection = db.collection(coll); 301 | collection.findOne({"_id": ObjectId(request.params.id)}, function(err, doc) { 302 | if(doc === null) { 303 | var error = Boom.badRequest('No doc found in '+coll); 304 | return reply(error); 305 | } 306 | else if(request.auth.isAuthenticated && request.auth.credentials.access !== 'admin' && doc.uId !== request.auth.artifacts.id) { 307 | var error = Boom.unauthorized('You are not permitted to delete this'); 308 | return reply(error); 309 | } 310 | else { 311 | collection.remove( _del, function(err) { 312 | 313 | return reply({error:null,message:'Deleted successfully'}); 314 | }); 315 | } 316 | }); 317 | } 318 | } 319 | }; 320 | return CRUD; 321 | } 322 | -------------------------------------------------------------------------------- /test/crud.js: -------------------------------------------------------------------------------- 1 | var Lab = require("lab"), 2 | Hapi = require("hapi"), 3 | Joi = require("joi"), 4 | MongoDB = require('mongodb').Db, 5 | Server = require('mongodb').Server, 6 | ObjectId = require('mongodb').ObjectID, 7 | Bcrypt = require('bcryptjs'), 8 | qs = require("querystring"); 9 | 10 | // Internal config stuff 11 | var CRUD = { 12 | collection: 'resources2', 13 | create: { 14 | bcrypt: 'password', 15 | date: 'created', 16 | payload: Joi.object().keys({ 17 | email: Joi.string().required(), 18 | password: Joi.string().required() 19 | }), 20 | defaults: { 21 | access: 'normal', 22 | activated: false 23 | }, 24 | }, 25 | update: { 26 | bcrypt: 'password', 27 | date: 'updated', 28 | payload: Joi.object().keys({ 29 | email: Joi.string(), 30 | password: Joi.string() 31 | }) 32 | }, 33 | validationOpts: { 34 | abortEarly: false 35 | } 36 | }; 37 | 38 | 39 | // Test shortcuts 40 | var lab = exports.lab = Lab.script(); 41 | var before = lab.before; 42 | var after = lab.after; 43 | var describe = lab.describe; 44 | var it = lab.it; 45 | var expect = Lab.expect; 46 | 47 | describe("Toothache", function() { 48 | 49 | var server = new Hapi.Server(); 50 | 51 | before(function (done) { 52 | var MongoClient = require('mongodb').MongoClient 53 | MongoClient.connect('mongodb://127.0.0.1:27017/test', function(err, db) { 54 | expect(err).to.not.exist; 55 | 56 | // Construct Resource 57 | CRUD.db = db; 58 | var Resource = require('../')(CRUD); 59 | 60 | // Get All 61 | server.route({ 62 | method: 'GET', path: '/api/resource', 63 | config: { 64 | handler: Resource.find 65 | } 66 | }); 67 | 68 | // Create 69 | server.route({ 70 | method: 'POST', path: '/api/resource', 71 | config: { 72 | handler: Resource.create 73 | } 74 | }); 75 | 76 | server.route({ 77 | method: 'GET', path: '/api/resource/create', 78 | config: { 79 | handler: Resource.create 80 | } 81 | }); 82 | 83 | // Get a resource 84 | server.route({ 85 | method: 'GET', path: '/api/resource/{id}', 86 | config: { 87 | handler: Resource.get 88 | } 89 | }); 90 | 91 | // Find 92 | server.route({ 93 | method: 'POST', path: '/api/resource/find', 94 | config: { 95 | handler: Resource.find 96 | } 97 | }); 98 | 99 | // Update 100 | server.route({ 101 | method: 'PUT', path: '/api/resource/{id}', 102 | config: { 103 | handler: Resource.update 104 | } 105 | }); 106 | 107 | server.route({ 108 | method: 'GET', path: '/api/resource/{id}/update', 109 | config: { 110 | handler: Resource.update 111 | } 112 | }); 113 | 114 | // Delete 115 | server.route({ 116 | method: 'DELETE', path: '/api/resource/{id}', 117 | config: { 118 | handler: Resource.del 119 | } 120 | }); 121 | 122 | done(); 123 | 124 | }) 125 | }); // Done with before 126 | 127 | it("creates a resource from POST", function(done) { 128 | var payload = { 129 | email: "test@test.com", 130 | password: "newpass" 131 | }; 132 | 133 | var options = { 134 | method: "POST", 135 | url: "/api/resource", 136 | payload: JSON.stringify(payload) 137 | }; 138 | 139 | server.inject(options, function(response) { 140 | var result = response.result; 141 | 142 | expect(response.statusCode).to.equal(200); 143 | expect(result).to.be.instanceof(Object); 144 | expect(result.access).to.equal('normal'); 145 | expect(result.activated).to.equal(false); 146 | expect(result.created).to.be.instanceof(Date); 147 | // Test password was bcrypted correctly 148 | var validPass = Bcrypt.compareSync(payload.password, result.password); 149 | expect(validPass).to.equal(true); 150 | 151 | done(); 152 | }); 153 | }) 154 | 155 | it("creates a resource from GET", function(done) { 156 | var payload = { 157 | email: "test3@test.com", 158 | password: "newpass3" 159 | }; 160 | 161 | var options = { 162 | method: "GET", 163 | url: "/api/resource/create?"+qs.stringify(payload) 164 | }; 165 | 166 | server.inject(options, function(response) { 167 | var result = response.result; 168 | 169 | expect(response.statusCode).to.equal(200); 170 | expect(result).to.be.instanceof(Object); 171 | expect(result.access).to.equal('normal'); 172 | expect(result.activated).to.equal(false); 173 | expect(result.created).to.be.instanceof(Date); 174 | // Test password was bcrypted correctly 175 | var validPass = Bcrypt.compareSync(payload.password, result.password); 176 | expect(validPass).to.equal(true); 177 | 178 | done(); 179 | }); 180 | }) 181 | 182 | it("lists all resources", function(done) { 183 | 184 | server.inject("/api/resource", function(response) { 185 | var result = response.result; 186 | 187 | expect(response.statusCode).to.equal(200); 188 | expect(result).to.be.instanceof(Array); 189 | // expect(result).to.have.length(1); 190 | 191 | done(); 192 | }); 193 | }); 194 | 195 | it("get a resource", function(done) { 196 | // Get all resources 197 | server.inject("/api/resource", function(response) { 198 | var result = response.result; 199 | server.inject("/api/resource/"+result[0]._id, function(response) { 200 | var result = response.result; 201 | expect(response.statusCode).to.equal(200); 202 | expect(result).to.be.instanceof(Object); 203 | // expect(result).to.have.length(1); 204 | 205 | done(); 206 | }); 207 | }); 208 | }); 209 | 210 | it("update a resource from POST", function(done) { 211 | // Get all resources 212 | server.inject("/api/resource", function(response) { 213 | var result = response.result; 214 | var payload = { 215 | email: "test2@test.com", 216 | password: "newpass2" 217 | }; 218 | 219 | var options = { 220 | method: "PUT", 221 | url: "/api/resource/"+result[0]._id, 222 | payload: JSON.stringify(payload) 223 | }; 224 | // Update resource 225 | server.inject(options, function(response) { 226 | var result = response.result; 227 | 228 | expect(response.statusCode).to.equal(200); 229 | expect(result).to.be.instanceof(Object); 230 | expect(result.message).to.equal('Updated successfully'); 231 | 232 | // Get updated resource 233 | server.inject(options.url, function(response) { 234 | var result = response.result; 235 | 236 | expect(result.email).to.equal(payload.email); 237 | expect(result.updated).to.be.instanceof(Date); 238 | 239 | // Test password was bcrypted correctly 240 | var validPass = Bcrypt.compareSync(payload.password, result.password); 241 | expect(validPass).to.equal(true); 242 | 243 | done(); 244 | }) 245 | }); 246 | }); 247 | }); 248 | 249 | it("update a resource from GET", function(done) { 250 | // Get all resources 251 | server.inject("/api/resource", function(response) { 252 | var result = response.result; 253 | var payload = { 254 | email: "test2@test.com", 255 | password: "newpass2" 256 | }; 257 | 258 | var options = { 259 | method: "GET", 260 | url: "/api/resource/"+result[0]._id+"/update?"+qs.stringify(payload) 261 | }; 262 | // Update resource 263 | server.inject(options, function(response) { 264 | var result = response.result; 265 | 266 | expect(response.statusCode).to.equal(200); 267 | expect(result).to.be.instanceof(Object); 268 | expect(result.message).to.equal('Updated successfully'); 269 | 270 | done(); 271 | }); 272 | }); 273 | }); 274 | 275 | it("delete a resource", function(done) { 276 | // Get all resources 277 | server.inject("/api/resource", function(response) { 278 | var result = response.result; 279 | var options = { 280 | method: "DELETE", 281 | url: "/api/resource/"+result[0]._id 282 | }; 283 | server.inject(options, function(response) { 284 | var result = response.result; 285 | expect(response.statusCode).to.equal(200); 286 | expect(result).to.be.instanceof(Object); 287 | expect(result.message).to.equal('Deleted successfully'); 288 | 289 | done(); 290 | }); 291 | }); 292 | }); 293 | 294 | it("finds a resource based on payload", function(done) { 295 | 296 | // Insert 2 resources 297 | for(var i = 1; i < 3; i++) { 298 | var payload = { 299 | email: "test"+i+"@acme.com", 300 | password: "newpass" 301 | }; 302 | 303 | var options = { 304 | method: "POST", 305 | url: "/api/resource", 306 | payload: JSON.stringify(payload) 307 | }; 308 | 309 | server.inject(options, function(response) {}) 310 | } 311 | 312 | var payload = { 313 | email: "test1@acme.com" 314 | }; 315 | 316 | var options = { 317 | method: "POST", 318 | url: "/api/resource/find", 319 | payload: JSON.stringify(payload) 320 | }; 321 | 322 | server.inject(options, function(response) { 323 | var result = response.result; 324 | 325 | expect(response.statusCode).to.equal(200); 326 | expect(result).to.be.instanceof(Array); 327 | expect(result).to.have.length(1); 328 | 329 | done(); 330 | }); 331 | }); 332 | 333 | it("whitelist filters fields for multiple docs", function(done) { 334 | CRUD.read = { 335 | whitelist: ['_id','email'] 336 | }; 337 | 338 | var Resource = require('../')(CRUD); 339 | 340 | // Get All 341 | server.route({ 342 | method: 'GET', path: '/api/resource/whitelist', 343 | config: { 344 | handler: Resource.find 345 | } 346 | }); 347 | 348 | server.inject('/api/resource/whitelist', function(response) { 349 | var result = response.result; 350 | 351 | expect(response.statusCode).to.equal(200); 352 | expect(result[0]).to.be.instanceof(Object); 353 | expect(typeof result[0].email).to.equal('string'); 354 | expect(result[0].password).to.not.exist; 355 | 356 | 357 | done(); 358 | }); 359 | }) 360 | 361 | it("whitelist filters fields for multiple docs", function(done) { 362 | CRUD.read = { 363 | whitelist: ['_id','email','boom'] 364 | }; 365 | 366 | var Resource = require('../')(CRUD); 367 | 368 | // Get All 369 | server.route({ 370 | method: 'GET', path: '/api/resource/whitelist2', 371 | config: { 372 | handler: Resource.find 373 | } 374 | }); 375 | 376 | server.inject('/api/resource/whitelist2', function(response) { 377 | var result = response.result; 378 | 379 | expect(response.statusCode).to.equal(200); 380 | expect(result[0]).to.be.instanceof(Object); 381 | expect(typeof result[0].email).to.equal('string'); 382 | expect(result[0].password).to.not.exist; 383 | expect(result[0].boom).to.not.exist; 384 | 385 | done(); 386 | }); 387 | }) 388 | 389 | it("whitelist filters fields for ind doc", function(done) { 390 | CRUD.read = { 391 | whitelist: ['_id','email'] 392 | }; 393 | 394 | var Resource = require('../')(CRUD); 395 | 396 | // Get All 397 | server.route({ 398 | method: 'GET', path: '/api/resource/{id}/whitelist', 399 | config: { 400 | handler: Resource.get 401 | } 402 | }); 403 | 404 | server.inject('/api/resource', function(response) { 405 | var id = response.result[0]['_id']; 406 | 407 | server.inject('/api/resource/'+id+'/whitelist', function(response) { 408 | var result = response.result; 409 | 410 | expect(response.statusCode).to.equal(200); 411 | expect(result).to.be.instanceof(Object); 412 | expect(typeof result.email).to.equal('string'); 413 | expect(result.password).to.not.exist; 414 | 415 | done(); 416 | }) 417 | 418 | }); 419 | }); 420 | 421 | it("whitelist doesn't add undefined fields for ind doc", function(done) { 422 | CRUD.read = { 423 | whitelist: ['_id','email','boom'] 424 | }; 425 | 426 | var Resource = require('../')(CRUD); 427 | 428 | // Get All 429 | server.route({ 430 | method: 'GET', path: '/api/resource/{id}/wlist', 431 | config: { 432 | handler: Resource.get 433 | } 434 | }); 435 | 436 | server.inject('/api/resource', function(response) { 437 | var id = response.result[0]['_id']; 438 | 439 | server.inject('/api/resource/'+id+'/wlist', function(response) { 440 | var result = response.result; 441 | 442 | expect(response.statusCode).to.equal(200); 443 | expect(result).to.be.instanceof(Object); 444 | expect(typeof result.email).to.equal('string'); 445 | expect(result.password).to.not.exist; 446 | expect(result.boom).to.not.exist; 447 | 448 | done(); 449 | }) 450 | 451 | }); 452 | }) 453 | 454 | it("blacklist filters fields for multiple docs", function(done) { 455 | CRUD.read = { 456 | blacklist: ['password'] 457 | }; 458 | 459 | var Resource = require('../')(CRUD); 460 | 461 | // Get All 462 | server.route({ 463 | method: 'GET', path: '/api/resource/blacklist', 464 | config: { 465 | handler: Resource.find 466 | } 467 | }); 468 | 469 | server.inject('/api/resource/blacklist', function(response) { 470 | var result = response.result; 471 | 472 | expect(response.statusCode).to.equal(200); 473 | expect(result[0]).to.be.instanceof(Object); 474 | expect(typeof result[0].email).to.equal('string'); 475 | expect(result[0].password).to.not.exist; 476 | 477 | 478 | done(); 479 | }); 480 | }) 481 | 482 | it("blacklist filters fields for ind doc", function(done) { 483 | CRUD.read = { 484 | blacklist: ['password'] 485 | }; 486 | 487 | var Resource = require('../')(CRUD); 488 | 489 | // Get All 490 | server.route({ 491 | method: 'GET', path: '/api/resource/{id}/blacklist', 492 | config: { 493 | handler: Resource.get 494 | } 495 | }); 496 | 497 | server.inject('/api/resource', function(response) { 498 | var id = response.result[0]['_id']; 499 | 500 | server.inject('/api/resource/'+id+'/blacklist', function(response) { 501 | var result = response.result; 502 | 503 | expect(response.statusCode).to.equal(200); 504 | expect(result).to.be.instanceof(Object); 505 | expect(typeof result.email).to.equal('string'); 506 | expect(result.password).to.not.exist; 507 | 508 | done(); 509 | }) 510 | 511 | }); 512 | }) 513 | 514 | }); -------------------------------------------------------------------------------- /test/access.js: -------------------------------------------------------------------------------- 1 | var Lab = require("lab"), 2 | Hapi = require("hapi"), 3 | Joi = require("joi"), 4 | Hawk = require("hawk"), 5 | MongoDB = require('mongodb').Db, 6 | Server = require('mongodb').Server, 7 | ObjectId = require('mongodb').ObjectID, 8 | Bcrypt = require('bcryptjs'); 9 | 10 | 11 | // Test shortcuts 12 | var lab = exports.lab = Lab.script(); 13 | var before = lab.before; 14 | var beforeEach = lab.beforeEach; 15 | var after = lab.after; 16 | var describe = lab.describe; 17 | var it = lab.it; 18 | var expect = Lab.expect; 19 | 20 | var credentials = { 21 | admin: { 22 | id: "user1", 23 | key: "pass1", 24 | algorithm: 'sha256' 25 | }, 26 | normal: { 27 | id: "user2", 28 | key: "pass2", 29 | algorithm: 'sha256' 30 | } 31 | } 32 | 33 | var CRUD = { 34 | collection: 'resources1', 35 | create: { 36 | payload: Joi.object().keys({ 37 | field: Joi.string().required(), 38 | }), 39 | defaults: { 40 | uId: true 41 | }, 42 | access: 'admin' 43 | }, 44 | update: { 45 | payload: Joi.object().keys({ 46 | field: Joi.string() 47 | }), 48 | access: 'normal' 49 | }, 50 | validationOpts: { 51 | abortEarly: false 52 | } 53 | }; 54 | 55 | describe("Toothache", function() { 56 | 57 | var server; 58 | 59 | beforeEach(function (done) { 60 | server = new Hapi.Server(); 61 | var MongoClient = require('mongodb').MongoClient 62 | MongoClient.connect('mongodb://127.0.0.1:27017/test', function(err, db) { 63 | expect(err).to.not.exist; 64 | 65 | // User config stuff 66 | CRUD.db = db; 67 | // Construct User CRUD 68 | var Resource = require('../')(CRUD); 69 | 70 | 71 | server.pack.register([ 72 | { 73 | name: 'hawk-auth', 74 | plugin: require('hapi-auth-hawk') 75 | } 76 | ], function(err) { 77 | if (err) throw err; 78 | server.auth.strategy('web', 'hawk', 79 | { 80 | getCredentialsFunc: function (id, callback) { 81 | // Core creds 82 | var credentials = { 83 | user1: { 84 | key: 'pass1', 85 | access: 'admin', 86 | algorithm: 'sha256' 87 | }, 88 | user2: { 89 | key: 'pass2', 90 | access: 'normal', 91 | algorithm: 'sha256' 92 | } 93 | } 94 | return callback(null, credentials[id]); 95 | } 96 | }); 97 | 98 | 99 | // Get all resources 100 | server.route({ 101 | method: 'GET', path: '/api/resource', 102 | config: { 103 | auth: 'web', 104 | handler: Resource.find 105 | } 106 | }); 107 | 108 | server.route({ 109 | method: 'POST', path: '/api/resource/find', 110 | config: { 111 | auth: 'web', 112 | handler: Resource.find 113 | } 114 | }); 115 | 116 | // Get a resource 117 | server.route({ 118 | method: 'GET', path: '/api/resource/{id}', 119 | config: { 120 | auth: 'web', 121 | handler: Resource.get 122 | } 123 | }); 124 | 125 | // Update 126 | server.route({ 127 | method: 'PUT', path: '/api/resource/{id}', 128 | config: { 129 | auth: 'web', 130 | handler: Resource.update 131 | } 132 | }); 133 | 134 | // Delete 135 | server.route({ 136 | method: 'DELETE', path: '/api/resource/{id}', 137 | config: { 138 | auth: 'web', 139 | handler: Resource.del 140 | } 141 | }); 142 | 143 | done(); 144 | }); 145 | }) 146 | }); // Done with before 147 | 148 | 149 | it("admin can create admin protected resource", function(done) { 150 | var Resource = require('../')(CRUD); 151 | // Create 152 | server.route({ 153 | method: 'POST', path: '/api/resource', 154 | config: { 155 | auth: 'web', 156 | handler: Resource.create 157 | } 158 | }); 159 | 160 | var payload = { 161 | field: "some value" 162 | }; 163 | 164 | var options = { 165 | method: "POST", 166 | url: "http://localhost.com/api/resource", 167 | payload: JSON.stringify(payload), 168 | headers: {} 169 | }; 170 | 171 | var counter = 0; 172 | 173 | // Add auth 174 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 175 | options.headers.Authorization = header.field; 176 | 177 | server.inject(options, function(response) { 178 | var result = response.result; 179 | 180 | expect(response.statusCode).to.equal(200); 181 | expect(result).to.be.instanceof(Object); 182 | expect(result.uId).to.equal(credentials.admin.id); 183 | 184 | done() 185 | }); 186 | 187 | server.inject(options, function(response) {}); 188 | server.inject(options, function(response) {}); 189 | }); 190 | 191 | it("non-admin can't create admin protected resource", function(done) { 192 | var Resource = require('../')(CRUD); 193 | // Create 194 | server.route({ 195 | method: 'POST', path: '/api/resource', 196 | config: { 197 | auth: 'web', 198 | handler: Resource.create 199 | } 200 | }); 201 | 202 | var payload = { 203 | field: "some value" 204 | }; 205 | 206 | var options = { 207 | method: "POST", 208 | url: "http://localhost.com/api/resource", 209 | payload: JSON.stringify(payload), 210 | headers: {} 211 | }; 212 | 213 | var counter = 0; 214 | 215 | // Add auth 216 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 217 | options.headers.Authorization = header.field; 218 | 219 | server.inject(options, function(response) { 220 | var result = response.result; 221 | 222 | expect(response.statusCode).to.equal(401); 223 | expect(result).to.be.instanceof(Object); 224 | expect(result.message).to.equal("You do not have create access"); 225 | 226 | done(); 227 | }); 228 | }) 229 | 230 | it("normal user can create normal protected resource", function(done) { 231 | 232 | CRUD.create.access = 'normal'; 233 | var Resource = require('../')(CRUD); 234 | // Create 235 | server.route({ 236 | method: 'POST', path: '/api/resource', 237 | config: { 238 | auth: 'web', 239 | handler: Resource.create 240 | } 241 | }); 242 | 243 | var payload = { 244 | field: "some value" 245 | }; 246 | 247 | var options = { 248 | method: "POST", 249 | url: "http://localhost.com/api/resource", 250 | payload: JSON.stringify(payload), 251 | headers: {} 252 | }; 253 | 254 | var counter = 0; 255 | 256 | // Add auth 257 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 258 | options.headers.Authorization = header.field; 259 | 260 | server.inject(options, function(response) { 261 | var result = response.result; 262 | 263 | expect(response.statusCode).to.equal(200); 264 | expect(result).to.be.instanceof(Object); 265 | expect(result.uId).to.equal(credentials.normal.id); 266 | 267 | done() 268 | }); 269 | 270 | server.inject(options, function(response) {}) 271 | }) 272 | 273 | it("admin user gets all resources", function(done) { 274 | var options = { 275 | method: "GET", 276 | url: "http://localhost.com/api/resource", 277 | headers: {} 278 | }; 279 | // Add auth 280 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 281 | options.headers.Authorization = header.field; 282 | 283 | server.inject(options, function(response) { 284 | var result = response.result; 285 | 286 | expect(response.statusCode).to.equal(200); 287 | expect(result).to.be.instanceof(Array); 288 | expect(result).to.have.length(5); 289 | 290 | done() 291 | }); 292 | }); 293 | 294 | it("normal user gets some but not all resources", function(done) { 295 | var options = { 296 | method: "GET", 297 | url: "http://localhost.com/api/resource", 298 | headers: {} 299 | }; 300 | // Add auth 301 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 302 | options.headers.Authorization = header.field; 303 | 304 | server.inject(options, function(response) { 305 | var result = response.result; 306 | 307 | expect(response.statusCode).to.equal(200); 308 | expect(result).to.be.instanceof(Array); 309 | expect(result).to.have.length(2); 310 | 311 | done() 312 | }); 313 | }); 314 | 315 | it("normal user can't access other user's resource", function(done) { 316 | var options = { 317 | method: "GET", 318 | url: "http://localhost.com/api/resource", 319 | headers: {} 320 | }; 321 | // Add auth 322 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 323 | options.headers.Authorization = header.field; 324 | 325 | server.inject(options, function(response) { 326 | var result = response.result; 327 | var options = { 328 | method: "GET", 329 | url: "http://localhost.com/api/resource/"+result[0]._id, 330 | headers: {} 331 | }; 332 | // Add auth 333 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 334 | options.headers.Authorization = header.field; 335 | 336 | server.inject(options, function(response) { 337 | var result = response.result; 338 | 339 | expect(response.statusCode).to.equal(401); 340 | expect(result).to.be.instanceof(Object); 341 | expect(result.message).to.equal("You are not permitted to see this"); 342 | 343 | done(); 344 | }) 345 | }); 346 | }); 347 | 348 | it("admin user can access other user's resource", function(done) { 349 | var options = { 350 | method: "GET", 351 | url: "http://localhost.com/api/resource", 352 | headers: {} 353 | }; 354 | // Add auth 355 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 356 | options.headers.Authorization = header.field; 357 | 358 | server.inject(options, function(response) { 359 | var result = response.result; 360 | var options = { 361 | method: "GET", 362 | url: "http://localhost.com/api/resource/"+result[0]._id, 363 | headers: {} 364 | }; 365 | // Add auth 366 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 367 | options.headers.Authorization = header.field; 368 | 369 | server.inject(options, function(response) { 370 | var result = response.result; 371 | 372 | expect(response.statusCode).to.equal(200); 373 | expect(result).to.be.instanceof(Object); 374 | 375 | 376 | done(); 377 | }) 378 | }); 379 | }); 380 | 381 | it("normal user can't update other user's resource", function(done) { 382 | var options = { 383 | method: "GET", 384 | url: "http://localhost.com/api/resource", 385 | headers: {} 386 | }; 387 | // Add auth 388 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 389 | options.headers.Authorization = header.field; 390 | 391 | server.inject(options, function(response) { 392 | var result = response.result; 393 | var payload = { 394 | field: "some value" 395 | }; 396 | 397 | var options = { 398 | method: "PUT", 399 | url: "http://localhost.com/api/resource/"+result[0]._id, 400 | payload: JSON.stringify(payload), 401 | headers: {} 402 | }; 403 | // Add auth 404 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 405 | options.headers.Authorization = header.field; 406 | 407 | server.inject(options, function(response) { 408 | var result = response.result; 409 | 410 | expect(response.statusCode).to.equal(401); 411 | expect(result).to.be.instanceof(Object); 412 | expect(result.message).to.equal("You are not permitted to update this"); 413 | 414 | done(); 415 | }) 416 | }); 417 | }); 418 | 419 | it("admin user can update other user's resource", function(done) { 420 | var options = { 421 | method: "GET", 422 | url: "http://localhost.com/api/resource", 423 | headers: {} 424 | }; 425 | // Add auth 426 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 427 | options.headers.Authorization = header.field; 428 | 429 | server.inject(options, function(response) { 430 | var result = response.result; 431 | var payload = { 432 | field: "some value" 433 | }; 434 | 435 | var options = { 436 | method: "PUT", 437 | url: "http://localhost.com/api/resource/"+result[0]._id, 438 | payload: JSON.stringify(payload), 439 | headers: {} 440 | }; 441 | // Add auth 442 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 443 | options.headers.Authorization = header.field; 444 | 445 | server.inject(options, function(response) { 446 | var result = response.result; 447 | 448 | expect(response.statusCode).to.equal(200); 449 | expect(result).to.be.instanceof(Object); 450 | 451 | done(); 452 | }) 453 | }); 454 | }); 455 | 456 | it("normal user can't delete other user's resource", function(done) { 457 | var options = { 458 | method: "GET", 459 | url: "http://localhost.com/api/resource", 460 | headers: {} 461 | }; 462 | // Add auth 463 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 464 | options.headers.Authorization = header.field; 465 | 466 | server.inject(options, function(response) { 467 | var result = response.result; 468 | var options = { 469 | method: "DELETE", 470 | url: "http://localhost.com/api/resource/"+result[0]._id, 471 | headers: {} 472 | }; 473 | // Add auth 474 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 475 | options.headers.Authorization = header.field; 476 | 477 | server.inject(options, function(response) { 478 | var result = response.result; 479 | 480 | expect(response.statusCode).to.equal(401); 481 | expect(result).to.be.instanceof(Object); 482 | expect(result.message).to.equal("You are not permitted to delete this"); 483 | 484 | done(); 485 | }) 486 | }); 487 | }); 488 | 489 | it("admin user can delete other user's resource", function(done) { 490 | var options = { 491 | method: "POST", 492 | url: "http://localhost.com/api/resource/find", 493 | payload: JSON.stringify({uId:'user2'}), 494 | headers: {} 495 | }; 496 | // Add auth 497 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 498 | options.headers.Authorization = header.field; 499 | 500 | server.inject(options, function(response) { 501 | var result = response.result; 502 | var options = { 503 | method: "DELETE", 504 | url: "http://localhost.com/api/resource/"+result[0]._id, 505 | headers: {} 506 | }; 507 | // Add auth 508 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.admin }); 509 | options.headers.Authorization = header.field; 510 | 511 | server.inject(options, function(response) { 512 | var result = response.result; 513 | 514 | expect(response.statusCode).to.equal(200); 515 | expect(result).to.be.instanceof(Object); 516 | expect(result.message).to.equal("Deleted successfully"); 517 | 518 | done(); 519 | }) 520 | }); 521 | }); 522 | 523 | it("non-admin can't get admin protected resource", function(done) { 524 | CRUD.read = { 525 | access: 'admin' 526 | } 527 | var Resource = require('../')(CRUD); 528 | // Create 529 | server.route({ 530 | method: 'GET', path: '/resource/{id}', 531 | config: { 532 | auth: 'web', 533 | handler: Resource.get 534 | } 535 | }); 536 | 537 | var options = { 538 | method: "GET", 539 | url: "http://localhost.com/resource/1", 540 | headers: {} 541 | }; 542 | 543 | 544 | // Add auth 545 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 546 | options.headers.Authorization = header.field; 547 | 548 | server.inject(options, function(response) { 549 | var result = response.result; 550 | 551 | expect(response.statusCode).to.equal(401); 552 | expect(result).to.be.instanceof(Object); 553 | expect(result.message).to.equal("You do not have read access"); 554 | 555 | done(); 556 | }); 557 | }) 558 | 559 | it("non-admin can't find admin protected resource", function(done) { 560 | CRUD.read = { 561 | access: 'admin' 562 | } 563 | var Resource = require('../')(CRUD); 564 | // Create 565 | server.route({ 566 | method: 'POST', path: '/resource/find', 567 | config: { 568 | auth: 'web', 569 | handler: Resource.find 570 | } 571 | }); 572 | 573 | var options = { 574 | method: "POST", 575 | url: "http://localhost.com/resource/find", 576 | headers: {} 577 | }; 578 | 579 | 580 | // Add auth 581 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 582 | options.headers.Authorization = header.field; 583 | 584 | server.inject(options, function(response) { 585 | var result = response.result; 586 | 587 | expect(response.statusCode).to.equal(401); 588 | expect(result).to.be.instanceof(Object); 589 | expect(result.message).to.equal("You do not have read access"); 590 | 591 | done(); 592 | }); 593 | }) 594 | 595 | it("non-admin can't update admin protected resource", function(done) { 596 | CRUD.update = { 597 | access: 'admin' 598 | } 599 | var Resource = require('../')(CRUD); 600 | // Create 601 | server.route({ 602 | method: 'PUT', path: '/resource/{id}', 603 | config: { 604 | auth: 'web', 605 | handler: Resource.update 606 | } 607 | }); 608 | 609 | var options = { 610 | method: "PUT", 611 | url: "http://localhost.com/resource/1", 612 | payload: JSON.stringify({uId:'user2'}), 613 | headers: {} 614 | }; 615 | 616 | 617 | // Add auth 618 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 619 | options.headers.Authorization = header.field; 620 | 621 | server.inject(options, function(response) { 622 | var result = response.result; 623 | 624 | expect(response.statusCode).to.equal(401); 625 | expect(result).to.be.instanceof(Object); 626 | expect(result.message).to.equal("You do not have update access"); 627 | 628 | done(); 629 | }); 630 | }) 631 | 632 | it("non-admin can't delete admin protected resource", function(done) { 633 | CRUD.del = { 634 | access: 'admin' 635 | } 636 | var Resource = require('../')(CRUD); 637 | // Create 638 | server.route({ 639 | method: 'DELETE', path: '/resource/{id}', 640 | config: { 641 | auth: 'web', 642 | handler: Resource.del 643 | } 644 | }); 645 | 646 | var options = { 647 | method: "DELETE", 648 | url: "http://localhost.com/resource/1", 649 | headers: {} 650 | }; 651 | 652 | 653 | // Add auth 654 | var header = Hawk.client.header(options.url, options.method, { credentials: credentials.normal }); 655 | options.headers.Authorization = header.field; 656 | 657 | server.inject(options, function(response) { 658 | var result = response.result; 659 | 660 | expect(response.statusCode).to.equal(401); 661 | expect(result).to.be.instanceof(Object); 662 | expect(result.message).to.equal("You do not have delete access"); 663 | 664 | done(); 665 | }); 666 | }) 667 | 668 | 669 | }); --------------------------------------------------------------------------------