├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── book.json ├── dist ├── getRoles.js ├── index.js ├── isAllowed.js ├── isAuthorized.js ├── lens.js ├── screen.js └── screenDeep.js ├── docs ├── Authorized.md ├── Endpoint.md ├── Introduction.md ├── Roles.md ├── Rules.md ├── Screen.md └── screenDeep.md ├── package.json ├── src ├── getRoles.js ├── index.js ├── isAllowed.js ├── isAuthorized.js ├── lens.js ├── screen.js └── screenDeep.js └── test ├── fixtures └── createModel.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | _book 12 | 13 | pids 14 | logs 15 | results 16 | 17 | npm-debug.log 18 | node_modules 19 | *.sublime* 20 | *.node 21 | coverage 22 | *.orig 23 | .idea 24 | sandbox 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | env: 5 | - CXX=g++-4.8 6 | addons: 7 | apt: 8 | sources: 9 | - ubuntu-toolchain-r-test 10 | packages: 11 | - g++-4.8 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Contra 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Role-based security, authorization, and filtering utilities for Thinky/RethinkDB

4 |

5 | 6 | ## Install 7 | 8 | One command and you're ready to secure your data: 9 | 10 | ``` 11 | npm install palisade --save 12 | ``` 13 | 14 | **Now**, check out the [documentation](http://shasta.tools/palisade/docs/Rules.html) to get started! 15 | 16 | ## Example 17 | 18 | The documentation has more detailed examples, but here's a quick peek: 19 | 20 | ### ES6 21 | 22 | ```js 23 | // your thinky connection instance 24 | import db from 'connections/thinky' 25 | import palisade, { screenDeep } from 'palisade' 26 | 27 | const User = db.createModel('User', { 28 | id: String, 29 | name: String, 30 | birthday: Date, 31 | times: { 32 | created: Date 33 | } 34 | }) 35 | 36 | // Anyone can list and read users and their public fields (id and name) 37 | // Users can update themselves, but only their own birthday 38 | // Admins can create, update, replace, or delete any user 39 | palisade(User, { 40 | document: { 41 | read: ['public'], 42 | create: ['admin'], 43 | update: ['admin', 'self'], 44 | replace: ['admin'], 45 | delete: ['admin'] 46 | }, 47 | read: { 48 | id: ['public'], 49 | name: ['public'], 50 | birthday: ['admin', 'self'], 51 | times: { 52 | created: ['admin'] 53 | } 54 | }, 55 | write: { 56 | id: ['admin'], 57 | name: ['admin'], 58 | birthday: ['admin', 'self'], 59 | times: { 60 | created: ['admin'] 61 | } 62 | } 63 | }) 64 | ``` 65 | 66 | ### ES5 67 | 68 | ```js 69 | // your thinky connection instance 70 | var db = require('connections/thinky'); 71 | var palisade = require('palisade'); 72 | var screenDeep = palisade.screenDeep; 73 | 74 | var User = db.createModel('User', { 75 | id: String, 76 | name: String, 77 | birthday: Date, 78 | times: { 79 | created: Date 80 | } 81 | }); 82 | 83 | // Anyone can list and read users and their public fields (id and name) 84 | // Users can update themselves, but only their own birthday 85 | // Admins can create, update, replace, or delete any user 86 | palisade(User, { 87 | document: { 88 | read: ['public'], 89 | create: ['admin'], 90 | update: ['admin', 'self'], 91 | replace: ['admin'], 92 | delete: ['admin'] 93 | }, 94 | read: { 95 | id: ['public'], 96 | name: ['public'], 97 | birthday: ['admin', 'self'], 98 | times: { 99 | created: ['admin'] 100 | } 101 | }, 102 | write: { 103 | id: ['admin'], 104 | name: ['admin'], 105 | birthday: ['admin', 'self'], 106 | times: { 107 | created: ['admin'] 108 | } 109 | } 110 | }); 111 | ``` 112 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "gitbook": "2.4.3", 3 | "structure": { 4 | "summary": "docs/Introduction.md" 5 | }, 6 | "plugins": [ 7 | "edit-link", 8 | "prism", 9 | "-highlight", 10 | "github", 11 | "anchors", 12 | "collapsible-menu" 13 | ], 14 | "pluginsConfig": { 15 | "edit-link": { 16 | "base": "https://github.com/shastajs/palisade/tree/master", 17 | "label": "Edit This Page" 18 | }, 19 | "github": { 20 | "url": "https://github.com/shastajs/palisade/" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dist/getRoles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | exports.default = function (user, data) { 8 | var roles = ['public']; 9 | if (user) roles.push('loggedIn'); 10 | if (!user) return roles; // nothing left to do 11 | 12 | if (user.role) roles.push(user.role); 13 | if (user.roles) roles = roles.concat(user.roles); 14 | if (data && user.id === data.id) roles.push('self'); 15 | return roles; 16 | }; 17 | 18 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.screenDeep = undefined; 7 | 8 | var _screen = require('./screen'); 9 | 10 | var _screen2 = _interopRequireDefault(_screen); 11 | 12 | var _isAuthorized = require('./isAuthorized'); 13 | 14 | var _isAuthorized2 = _interopRequireDefault(_isAuthorized); 15 | 16 | var _screenDeep2 = require('./screenDeep'); 17 | 18 | var _screenDeep3 = _interopRequireDefault(_screenDeep2); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | exports.screenDeep = _screenDeep3.default; 23 | 24 | exports.default = function (Model, rules) { 25 | Model.security = rules; 26 | Model.defineStatic('authorized', _isAuthorized2.default.bind(null, Model.security)); 27 | Model.define('authorized', function (type, user) { 28 | return Model.authorized(type, user, this); 29 | }); 30 | Model.defineStatic('screen', _screen2.default.bind(null, Model.security)); 31 | Model.define('screen', function (type, user) { 32 | return Model.screen(type, user, this); 33 | }); 34 | return Model; 35 | }; -------------------------------------------------------------------------------- /dist/isAllowed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _lodash = require('lodash.some'); 8 | 9 | var _lodash2 = _interopRequireDefault(_lodash); 10 | 11 | var _getRoles = require('./getRoles'); 12 | 13 | var _getRoles2 = _interopRequireDefault(_getRoles); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | exports.default = function (rules, user, data) { 18 | if (!Array.isArray(rules) && typeof rules !== 'function') return false; 19 | var roles = (0, _getRoles2.default)(user, data); 20 | var fnOpt = { 21 | user: user, 22 | data: data, 23 | roles: roles 24 | }; 25 | 26 | // auth is a fn 27 | if (typeof rules === 'function') { 28 | return rules(fnOpt); 29 | } 30 | 31 | // auth is an array of strings or fns 32 | return (0, _lodash2.default)(rules, function (v) { 33 | if (typeof v === 'function') return v(fnOpt); 34 | if (typeof v === 'string') return roles.indexOf(v) !== -1; 35 | return false; 36 | }); 37 | }; 38 | 39 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/isAuthorized.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _isAllowed = require('./isAllowed'); 8 | 9 | var _isAllowed2 = _interopRequireDefault(_isAllowed); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | exports.default = function (rules, type, user, data) { 14 | if (!type) throw new Error('Missing operation type'); 15 | if (!rules.document) return false; 16 | return (0, _isAllowed2.default)(rules.document[type], user, data); 17 | }; 18 | 19 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/lens.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _typeof2 = require('babel-runtime/helpers/typeof'); 4 | 5 | var _typeof3 = _interopRequireDefault(_typeof2); 6 | 7 | var _lodash = require('lodash.reduce'); 8 | 9 | var _lodash2 = _interopRequireDefault(_lodash); 10 | 11 | var _lodash3 = require('lodash.isdate'); 12 | 13 | var _lodash4 = _interopRequireDefault(_lodash3); 14 | 15 | var _isAllowed = require('./isAllowed'); 16 | 17 | var _isAllowed2 = _interopRequireDefault(_isAllowed); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | var filterWithLens = function filterWithLens(schema, user, data) { 22 | if ((typeof data === 'undefined' ? 'undefined' : (0, _typeof3.default)(data)) !== 'object') return; 23 | if ((0, _lodash4.default)(data)) return data.toISOString(); 24 | return (0, _lodash2.default)(data, function (o, v, k) { 25 | if (!data.hasOwnProperty(k)) return; 26 | var rules = schema[k]; 27 | var needsNesting = (typeof rules === 'undefined' ? 'undefined' : (0, _typeof3.default)(rules)) === 'object' && !Array.isArray(rules); 28 | 29 | if (needsNesting) { 30 | if ((typeof v === 'undefined' ? 'undefined' : (0, _typeof3.default)(v)) === 'object') { 31 | o[k] = filterWithLens(rules, user, v); 32 | } 33 | } else if ((0, _isAllowed2.default)(rules, user, data)) { 34 | o[k] = v; 35 | } 36 | 37 | return o; 38 | }, {}); 39 | }; 40 | 41 | module.exports = filterWithLens; -------------------------------------------------------------------------------- /dist/screen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof2 = require('babel-runtime/helpers/typeof'); 8 | 9 | var _typeof3 = _interopRequireDefault(_typeof2); 10 | 11 | var _lodash = require('lodash.map'); 12 | 13 | var _lodash2 = _interopRequireDefault(_lodash); 14 | 15 | var _lens = require('./lens'); 16 | 17 | var _lens2 = _interopRequireDefault(_lens); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | var screen = function screen(rules, type, user, data) { 22 | if (Array.isArray(data)) { 23 | if (!rules[type]) return []; 24 | return (0, _lodash2.default)(data, screen.bind(null, rules, type, user)); 25 | } 26 | if (data && (typeof data === 'undefined' ? 'undefined' : (0, _typeof3.default)(data)) === 'object') { 27 | if (!rules[type]) return {}; 28 | return (0, _lens2.default)(rules[type], user, data); 29 | } 30 | }; 31 | 32 | exports.default = screen; 33 | module.exports = exports['default']; -------------------------------------------------------------------------------- /dist/screenDeep.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _typeof2 = require('babel-runtime/helpers/typeof'); 8 | 9 | var _typeof3 = _interopRequireDefault(_typeof2); 10 | 11 | var _lodash = require('lodash.reduce'); 12 | 13 | var _lodash2 = _interopRequireDefault(_lodash); 14 | 15 | var _lodash3 = require('lodash.isdate'); 16 | 17 | var _lodash4 = _interopRequireDefault(_lodash3); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | var screenDeep = function screenDeep(user, data, returnEmpty) { 22 | if ((0, _lodash4.default)(data)) return data.toISOString(); 23 | 24 | // check if the user can even see the doc 25 | if ((typeof data === 'undefined' ? 'undefined' : (0, _typeof3.default)(data)) === 'object' && !Array.isArray(data)) { 26 | if (data.authorized && !data.authorized('read', user)) { 27 | if (returnEmpty) return; 28 | return Array.isArray(data) ? [] : {}; 29 | } 30 | // single instance w/ lens 31 | if (data.screen) { 32 | return data.screen('read', user); 33 | } 34 | 35 | // object with values as data 36 | return (0, _lodash2.default)(data, function (p, v, k) { 37 | if (!data.hasOwnProperty(k)) return; 38 | var nv = screenDeep(user, v, true); 39 | if (typeof nv !== 'undefined') p[k] = nv; 40 | return p; 41 | }, {}); 42 | } 43 | 44 | // array of data 45 | if (Array.isArray(data)) { 46 | return (0, _lodash2.default)(data, function (p, v) { 47 | var nv = screenDeep(user, v, true); 48 | if (typeof nv !== 'undefined') p.push(nv); 49 | return p; 50 | }, []); 51 | } 52 | 53 | return data; 54 | }; 55 | 56 | exports.default = screenDeep; 57 | module.exports = exports['default']; -------------------------------------------------------------------------------- /docs/Authorized.md: -------------------------------------------------------------------------------- 1 | # Model.authorized(type, user, data) 2 | 3 | Palisade adds a `.authorized()` function to both the Model class as well as all instances of the model (via define and defineStatic). This function checks the document rules in your security schema and returns `true` if everything is cool. 4 | 5 | When using the instance method, the data argument is set to the instance. 6 | 7 | ## API 8 | 9 | - `type` argument is the type of operation being requested on the document 10 | - Can be any type you [specified in your document ruleset](docs/Rules.md) 11 | - `user` is the requesting user you want to [pull roles from](docs/Roles.md) 12 | - Optional 13 | - `data` is the document the operating is being requested for 14 | - Optional 15 | 16 | ### Considerations 17 | 18 | - If no rules are specified for a document operation type, defaults to denying access 19 | - If an object with an `id` attribute isn't given for the data argument, the `self` role can't be provided 20 | - *Hot tip:* If you don't have access to the whole document but you know the id, just `{id: 'the id'}` will work fine 21 | 22 | ## Read Example 23 | 24 | In this example, we have two users: one admin, and one moderator. The schema specifies that only admins can read user documents, so only the admin user gets a `true` return from calling `authorized()`. 25 | 26 | ```js 27 | // create and wire in the schema 28 | var schema = { 29 | document: { 30 | read: ['admin'] 31 | } 32 | } 33 | palisade(User, schema) 34 | 35 | // create some dummy data 36 | var me = { 37 | role: 'admin', 38 | name: 'Contra' 39 | } 40 | 41 | var them = { 42 | role: 'moderator', 43 | name: 'Todd' 44 | } 45 | 46 | // ask for a read on them from me 47 | var canRead = User.authorize('read', me, them) 48 | console.log(canRead) // true 49 | 50 | // ask for a read on me from them 51 | var canRead2 = User.authorize('read', them, me) 52 | console.log(canRead2) // false 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/Endpoint.md: -------------------------------------------------------------------------------- 1 | # API Endpoint 2 | 3 | To tie it all together, here's how you would use Palisade to create some secure API endpoints: 4 | 5 | ## Reading a User by Id 6 | 7 | ```js 8 | import palisade, {screenDeep} from 'palisade' 9 | import User from 'models/User' 10 | 11 | export default (req, res, next) => { 12 | // first check if the request is allowed to read 13 | // anything from the users collection 14 | if (!User.authorized('read', req.user)) { 15 | return res.status({status: 403}) 16 | } 17 | 18 | // run the query 19 | User.get(req.params.id).run(function(err, data){ 20 | if (err) return next(err) 21 | 22 | // send back our secured data! 23 | res.json(screenDeep(req.user, filtered)) 24 | }) 25 | } 26 | ``` 27 | 28 | ## Updating a User by Id 29 | 30 | ```js 31 | import palisade, {screenDeep} from 'palisade' 32 | import User from 'models/User' 33 | 34 | export default (req, res, next) => { 35 | // checks if the request is allowed to update the 36 | // document at all. provides the id to enable the `self` role to exist 37 | if (!User.authorized('update', req.user, {id: req.params.id})) { 38 | return res.status({status: 403}) 39 | } 40 | 41 | // screen the user-provided data 42 | var change = User.screen('write', req.user, req.body) 43 | 44 | // run the query to update 45 | User.get(req.params.id) 46 | .update(change, {returnChanges: true}) 47 | .execute((err, res) => { 48 | if (err) return next(err) 49 | 50 | // send back our new secured data! 51 | res.json(screenDeep(req.user, filtered)) 52 | }) 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/Introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | - [ACL Rules](docs/Rules.md) 4 | - [User Roles](docs/Roles.md) 5 | - [Model.authorized()](docs/Authorized.md) 6 | - [Model.screen()](docs/Screen.md) 7 | - [screenDeep()](docs/ScreenDeep.md) 8 | - [API Endpoint](docs/Endpoint.md) 9 | -------------------------------------------------------------------------------- /docs/Roles.md: -------------------------------------------------------------------------------- 1 | # User Roles 2 | 3 | [`Model.authorized()`](docs/Authorized.md), [`Model.screen()`](docs/Screen.md), and [`.screenDeep()`](docs/ScreenDeep.md) take an optional user object as an argument which can provide role information. 4 | 5 | ## Assumed Roles 6 | 7 | - `public` is always assumed 8 | - `loggedIn` is assumed if any user object was provided 9 | - `self` is assumed if the user provided has the same id as the document being accessed 10 | 11 | ## Other Roles 12 | 13 | - If a user has a `role` attribute, it will be included 14 | - This can either be a string, or a function that returns a boolean 15 | - Any functions given will receive an object as the sole argument, which contains `user`, `data`, and `roles` keys 16 | - If a user has a `roles` attribute, it will be included 17 | - Expects either an array of strings, or functions that returns a boolean 18 | - (or any combination of the two!) 19 | - Any functions given will receive an object as the sole argument, which contains `user`, `data`, and `roles` keys 20 | 21 | ## Naming Roles 22 | 23 | Your can name your roles however you like, but I typically use: 24 | 25 | - `public` (provided by palisade) 26 | - `self` (provided by palisade) 27 | - `loggedIn` (provided by palisade) 28 | - `admin` (people responsible for moderating or performing maintenance) 29 | - `root` (you, the engineer) 30 | -------------------------------------------------------------------------------- /docs/Rules.md: -------------------------------------------------------------------------------- 1 | # ACL Rules 2 | 3 | Palisade has three categories of security rules: 4 | 5 | ### `document` 6 | 7 | - Each key is an operation type (for example: list, read, create, delete, update) that can be performed at a document level 8 | - You may specify any number of operation types named however you like 9 | - Personally, I always use these ones as they translate well to REST endpoints: 10 | - `list` - Can they query for more than one document 11 | - `read` - Can they read a single document 12 | - `create` - Can they create a new document 13 | - `update` - Can they update fields on an existing document 14 | - `replace` - Can they completely replace an existing document 15 | - `delete` - Can they delete an existing document 16 | 17 | ### `read` 18 | 19 | - Each key is a field name that corresponds to your schema 20 | - The value is an array of roles that can update the field 21 | - The value can also be an object with more fields inside of it for nested data 22 | 23 | ### `write` 24 | 25 | - Each key is a field name that corresponds to your schema 26 | - The value is an array of roles that can update the field 27 | - The value can also be an object with more fields inside of it for nested data 28 | 29 | ## Example Schema 30 | 31 | In this example ruleset: 32 | 33 | - Anyone can list users 34 | - Anyone can read a single user 35 | - Anyone read a user's public fields `id` and `name` 36 | - Users can update themselves, but only their own birthday 37 | - Admins can create, update, replace, or delete any user 38 | 39 | ```js 40 | var userSchema = { 41 | document: { 42 | read: ['public'], 43 | create: ['admin'], 44 | update: ['admin', 'self'], 45 | replace: ['admin'], 46 | delete: ['admin'] 47 | }, 48 | read: { 49 | id: ['public'], 50 | name: ['public'], 51 | birthday: ['admin', 'self'], 52 | times: { 53 | created: ['admin'] 54 | } 55 | }, 56 | write: { 57 | id: ['admin'], 58 | name: ['admin'], 59 | birthday: ['admin', 'self'], 60 | times: { 61 | created: ['admin'] 62 | } 63 | } 64 | }; 65 | ``` 66 | 67 | If you don't define a category or a rule for a field or type, the default is to deny access. 68 | 69 | ## Plugging it in 70 | 71 | To add the security functionality to a [Thinky](http://thinky.io) model, pass the palisade function the model and your security schema. 72 | 73 | This does a few things: 74 | - Adds a [.screen()](Screen.md) function to the Model class and all Model instances 75 | - Adds a [.authorized()](Authorized.md) function to the Model class and all Model instances 76 | - Adds your security ruleset as a `.security` property on the Model class 77 | - This makes it easy for models to extend eachother's security rules 78 | 79 | ```js 80 | import db from 'connections/thinky' 81 | import palisade from 'palisade' 82 | 83 | const User = db.createModel('User', { 84 | id: String, 85 | name: String, 86 | birthday: Date, 87 | times: { 88 | created: Date 89 | } 90 | }) 91 | palisade(User, userSchema) 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/Screen.md: -------------------------------------------------------------------------------- 1 | # Model.screen(type, user, data) 2 | 3 | Palisade adds a `.screen()` function to both the Model class as well as all instances of the model (via define and defineStatic). This function recursively filters out fields based on your security schema and the requesting user. 4 | 5 | When using the instance method, the data argument is set to the instance. 6 | 7 | ## API 8 | 9 | - `type` argument is the type of access 10 | - Can be either `read` or `write` 11 | - `user` is the requesting user you want to [pull roles from](docs/Roles.md) 12 | - Optional 13 | - `data` is the data to be sanitized 14 | - Can be an object, array of objects, or 15 | 16 | ### Considerations 17 | 18 | - If no rules are specified for a field, defaults to denying access 19 | - If data is not an object or an array, it is simply returned 20 | 21 | ## Read Example 22 | 23 | In this example, we have two users: one admin, and one moderator. The schema specifies that only admins can read the name field, so only the admin user is able to see it. 24 | 25 | ```js 26 | // create and wire in the schema 27 | var schema = { 28 | document: { 29 | read: ['public'] 30 | }, 31 | read: { 32 | name: ['admin'] 33 | } 34 | } 35 | palisade(User, schema) 36 | 37 | // create some dummy data 38 | var me = { 39 | role: 'admin', 40 | name: 'Contra' 41 | } 42 | 43 | var them = { 44 | role: 'moderator', 45 | name: 'Todd' 46 | } 47 | 48 | // ask for a read on them from me 49 | var filtered = User.screen('read', me, them) 50 | console.log(filtered) // {name: 'Todd'} 51 | 52 | // ask for a read on me from them 53 | var filtered2 = User.screen('read', them, me) 54 | console.log(filtered2) // {} 55 | ``` 56 | 57 | ## Write Example 58 | 59 | In the same way that you can sanitize reads, you can also sanitize writes. Before inserting anything into the database or updating any documents, run all user-provided data through `.screen()` to ensure that any updates they don't have permission for are removed. 60 | 61 | In this example, we have two users: one admin, and one moderator. The schema specifies that only admins can write the name field, so the moderator provided data has that field removed. 62 | 63 | ```js 64 | // create and wire in the schema 65 | var schema = { 66 | document: { 67 | read: ['public'] 68 | }, 69 | write: { 70 | name: ['admin'] 71 | } 72 | } 73 | palisade(User, schema) 74 | 75 | // create some dummy data 76 | var me = { 77 | role: 'admin', 78 | name: 'Contra' 79 | } 80 | 81 | var them = { 82 | role: 'moderator', 83 | name: 'Todd' 84 | } 85 | 86 | // ask for a write on them from me 87 | var filtered = User.screen('write', me, them) 88 | console.log(filtered) // {name: 'Todd'} 89 | 90 | // ask for a write on me from them 91 | var filtered2 = User.screen('write', them, me) 92 | console.log(filtered2) // {} 93 | ``` 94 | -------------------------------------------------------------------------------- /docs/screenDeep.md: -------------------------------------------------------------------------------- 1 | # screenDeep(user, data) 2 | 3 | Palisade exports a `.screenDeep()` function that lets you ensure complex objects or arrays have all nested data within it secured. It recursively walks through data and runs `.screen()` + `.authorized()` on any objects that expose it. This is extremely useful for exposing sets of data that contain multiple types of models, or are nested in any way. 4 | 5 | ## API 6 | 7 | - `user` is the requesting user you want to [pull roles from](docs/Roles.md) 8 | - Optional 9 | - `data` is the data to be sanitized 10 | - Can be an object, array of objects, or 11 | 12 | ## Considerations 13 | 14 | - Objects and arrays will be recursively processed 15 | - Objects that expose a [`.screen()`](docs/Screen.md) function will be replaced with the output of that call with a type of `read` 16 | - Objects that expose a [`.authorized()`](docs/Authorized.md) function will be removed if the output of that call with a type of `read` is false 17 | - Objects removed from either `screen()` or `authorized()` will be stripped 18 | - If you have 20 items where the user can't see 10, it will return an array of 10 items 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "palisade", 3 | "version": "1.1.6", 4 | "description": "Role-based security, authorization, and filtering utilities for thinky and RethinkDB", 5 | "main": "dist/index.js", 6 | "keywords": [ 7 | "security", 8 | "authorization", 9 | "filter", 10 | "filtering", 11 | "thinky", 12 | "rethink", 13 | "rethinkdb", 14 | "api", 15 | "user", 16 | "role", 17 | "acl" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/shastajs/palisade.git" 22 | }, 23 | "author": "Contra (http://contra.io)", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/shastajs/palisade/issues" 27 | }, 28 | "homepage": "https://github.com/shastajs/palisade#readme", 29 | "files": [ 30 | "dist" 31 | ], 32 | "scripts": { 33 | "preversion": "npm run clean && npm run build && npm docs", 34 | "postversion": "npm run changelog", 35 | "build": "babel src --out-dir dist", 36 | "clean": "rimraf dist", 37 | "lint": "eslint src test", 38 | "changelog": "github-changes -o contra -r palisade -b master -f ./CHANGELOG.md --order-semver --use-commit-body", 39 | "test": "npm run-script lint && mocha --compilers js:babel-register --recursive --reporter spec", 40 | "docs": "npm run docs:pre && npm run docs:build && npm run docs:publish", 41 | "docs:pre": "gitbook install && rimraf _book", 42 | "docs:build": "gitbook build -g shastajs/palisade", 43 | "docs:publish": "cd _book && git init && git commit --allow-empty -m 'update book' && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am 'update book' && git push git@github.com:shastajs/palisade gh-pages --force" 44 | }, 45 | "devDependencies": { 46 | "babel": "^6.3.26", 47 | "babel-cli": "^6.4.0", 48 | "babel-core": "^6.4.0", 49 | "babel-eslint": "^7.0.0", 50 | "babel-loader": "^6.2.1", 51 | "babel-plugin-add-module-exports": "^0.2.1", 52 | "babel-plugin-transform-runtime": "^6.4.3", 53 | "babel-preset-es2015": "^6.3.13", 54 | "babel-preset-es2015-loose": "^8.0.0", 55 | "babel-preset-stage-0": "^6.3.13", 56 | "babel-register": "^6.4.3", 57 | "babelify": "^7.2.0", 58 | "eslint": "^3.8.1", 59 | "eslint-config-rackt": "^1.1.1", 60 | "gitbook-cli": "^2.3.0", 61 | "github-changes": "^1.0.1", 62 | "mocha": "^3.0.0", 63 | "rimraf": "^2.5.0", 64 | "should": "^11.0.0", 65 | "thinky": "^2.3.7" 66 | }, 67 | "babel": { 68 | "presets": [ 69 | "es2015", 70 | "stage-0" 71 | ], 72 | "plugins": [ 73 | "transform-runtime", 74 | "add-module-exports" 75 | ] 76 | }, 77 | "eslintConfig": { 78 | "parser": "babel-eslint", 79 | "extends": "rackt", 80 | "env": { 81 | "node": true, 82 | "es6": true, 83 | "mocha": true 84 | }, 85 | "ecmaFeatures": { 86 | "modules": true 87 | }, 88 | "globals": { 89 | "it": true, 90 | "describe": true 91 | } 92 | }, 93 | "dependencies": { 94 | "lodash.filter": "^4.0.1", 95 | "lodash.foreach": "^4.0.0", 96 | "lodash.includes": "^4.0.1", 97 | "lodash.intersection": "^4.0.1", 98 | "lodash.isdate": "^4.0.0", 99 | "lodash.isempty": "^4.0.0", 100 | "lodash.map": "^4.1.0", 101 | "lodash.mapvalues": "^4.0.1", 102 | "lodash.partial": "^4.0.1", 103 | "lodash.reduce": "^4.1.0", 104 | "lodash.some": "^4.1.0" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/getRoles.js: -------------------------------------------------------------------------------- 1 | export default (user, data) => { 2 | let roles = [ 'public' ] 3 | if (user) roles.push('loggedIn') 4 | if (!user) return roles // nothing left to do 5 | 6 | if (user.role) roles.push(user.role) 7 | if (user.roles) roles = roles.concat(user.roles) 8 | if (data && user.id === data.id) roles.push('self') 9 | return roles 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import screen from './screen' 2 | import isAuthorized from './isAuthorized' 3 | 4 | export screenDeep from './screenDeep' 5 | 6 | export default (Model, rules) => { 7 | Model.security = rules 8 | Model.defineStatic('authorized', isAuthorized.bind(null, Model.security)) 9 | Model.define('authorized', function (type, user) { 10 | return Model.authorized(type, user, this) 11 | }) 12 | Model.defineStatic('screen', screen.bind(null, Model.security)) 13 | Model.define('screen', function (type, user) { 14 | return Model.screen(type, user, this) 15 | }) 16 | return Model 17 | } 18 | -------------------------------------------------------------------------------- /src/isAllowed.js: -------------------------------------------------------------------------------- 1 | import some from 'lodash.some' 2 | import getRoles from './getRoles' 3 | 4 | export default (rules, user, data) => { 5 | if (!Array.isArray(rules) && typeof rules !== 'function') return false 6 | const roles = getRoles(user, data) 7 | const fnOpt = { 8 | user: user, 9 | data: data, 10 | roles: roles 11 | } 12 | 13 | // auth is a fn 14 | if (typeof rules === 'function') { 15 | return rules(fnOpt) 16 | } 17 | 18 | // auth is an array of strings or fns 19 | return some(rules, (v) => { 20 | if (typeof v === 'function') return v(fnOpt) 21 | if (typeof v === 'string') return roles.indexOf(v) !== -1 22 | return false 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/isAuthorized.js: -------------------------------------------------------------------------------- 1 | import isAllowed from './isAllowed' 2 | 3 | export default (rules, type, user, data) => { 4 | if (!type) throw new Error('Missing operation type') 5 | if (!rules.document) return false 6 | return isAllowed(rules.document[type], user, data) 7 | } 8 | -------------------------------------------------------------------------------- /src/lens.js: -------------------------------------------------------------------------------- 1 | import reduce from 'lodash.reduce' 2 | import isDate from 'lodash.isdate' 3 | import isAllowed from './isAllowed' 4 | 5 | const filterWithLens = (schema, user, data) => { 6 | if (typeof data !== 'object') return 7 | if (isDate(data)) return data.toISOString() 8 | return reduce(data, (o, v, k) => { 9 | if (!data.hasOwnProperty(k)) return 10 | const rules = schema[k] 11 | const needsNesting = typeof rules === 'object' && !Array.isArray(rules) 12 | 13 | if (needsNesting) { 14 | if (typeof v === 'object') { 15 | o[k] = filterWithLens(rules, user, v) 16 | } 17 | } else if (isAllowed(rules, user, data)) { 18 | o[k] = v 19 | } 20 | 21 | return o 22 | }, {}) 23 | } 24 | 25 | module.exports = filterWithLens 26 | -------------------------------------------------------------------------------- /src/screen.js: -------------------------------------------------------------------------------- 1 | import map from 'lodash.map' 2 | import lens from './lens' 3 | 4 | const screen = (rules, type, user, data) => { 5 | if (Array.isArray(data)) { 6 | if (!rules[type]) return [] 7 | return map(data, screen.bind(null, rules, type, user)) 8 | } 9 | if (data && typeof data === 'object') { 10 | if (!rules[type]) return {} 11 | return lens(rules[type], user, data) 12 | } 13 | } 14 | 15 | export default screen 16 | -------------------------------------------------------------------------------- /src/screenDeep.js: -------------------------------------------------------------------------------- 1 | import reduce from 'lodash.reduce' 2 | import isDate from 'lodash.isdate' 3 | 4 | const screenDeep = (user, data, returnEmpty) => { 5 | if (isDate(data)) return data.toISOString() 6 | 7 | // check if the user can even see the doc 8 | if (typeof data === 'object' && !Array.isArray(data)) { 9 | if (data.authorized && 10 | !data.authorized('read', user)) { 11 | if (returnEmpty) return 12 | return Array.isArray(data) ? [] : {} 13 | } 14 | // single instance w/ lens 15 | if (data.screen) { 16 | return data.screen('read', user) 17 | } 18 | 19 | // object with values as data 20 | return reduce(data, (p, v, k) => { 21 | if (!data.hasOwnProperty(k)) return 22 | let nv = screenDeep(user, v, true) 23 | if (typeof nv !== 'undefined') p[k] = nv 24 | return p 25 | }, {}) 26 | } 27 | 28 | // array of data 29 | if (Array.isArray(data)) { 30 | return reduce(data, (p, v) => { 31 | let nv = screenDeep(user, v, true) 32 | if (typeof nv !== 'undefined') p.push(nv) 33 | return p 34 | }, []) 35 | } 36 | 37 | return data 38 | } 39 | 40 | export default screenDeep 41 | -------------------------------------------------------------------------------- /test/fixtures/createModel.js: -------------------------------------------------------------------------------- 1 | import thinky from 'thinky' 2 | 3 | const db = thinky({ 4 | silent: true 5 | }) 6 | db.r.getPoolMaster()._flushErrors = () => {} 7 | 8 | export default (name, schema) => 9 | db.createModel(name, schema) 10 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /*global it: true, describe: true */ 2 | /*eslint no-console: 0*/ 3 | import should from 'should' 4 | import palisade, { screenDeep } from '../src' 5 | import createModel from './fixtures/createModel' 6 | 7 | const createUser = () => 8 | createModel(`User-${Math.random()}`, { 9 | name: String, 10 | bday: Date 11 | }) 12 | 13 | describe('palisade', () => { 14 | it('should export a plugin function', () => { 15 | should.exist(palisade) 16 | palisade.should.be.a.function 17 | }) 18 | it('should export a screenDeep', () => { 19 | should.exist(screenDeep) 20 | screenDeep.should.be.a.function 21 | }) 22 | 23 | it('should return a Model class', () => { 24 | let rules = {} 25 | let User = createUser() 26 | palisade(User, rules).should.equal(User) 27 | }) 28 | 29 | it('should add a security field to a Model class', () => { 30 | let rules = {} 31 | let User = createUser() 32 | palisade(User, rules) 33 | should.exist(User.security) 34 | User.security.should.equal(rules) 35 | }) 36 | }) 37 | 38 | describe('Model.authorized', () => { 39 | it('should exist on class', () => { 40 | let rules = {} 41 | let User = createUser() 42 | palisade(User, rules) 43 | should.exist(User.authorized) 44 | }) 45 | it('should exist on instance', () => { 46 | let rules = {} 47 | let User = createUser() 48 | palisade(User, rules) 49 | should.exist(new User({}).authorized) 50 | }) 51 | it('should return false if no document rules specified', () => { 52 | let rules = {} 53 | let User = createUser() 54 | palisade(User, rules) 55 | User.authorized('read').should.equal(false) 56 | }) 57 | it('should return false if no document rules for type specified', () => { 58 | let rules = { 59 | document: {} 60 | } 61 | let User = createUser() 62 | palisade(User, rules) 63 | User.authorized('read').should.equal(false) 64 | }) 65 | it('should return false if document rules specified have no roles', () => { 66 | let rules = { 67 | document: { 68 | read: [] 69 | } 70 | } 71 | let User = createUser() 72 | palisade(User, rules) 73 | User.authorized('read').should.equal(false) 74 | }) 75 | it('should return true if document rules are public and no user given', () => { 76 | let rules = { 77 | document: { 78 | read: [ 'public' ] 79 | } 80 | } 81 | let User = createUser() 82 | palisade(User, rules) 83 | User.authorized('read').should.equal(true) 84 | }) 85 | it('should return false if user role attr doesnt match', () => { 86 | let rules = { 87 | document: { 88 | read: [ 'admin' ] 89 | } 90 | } 91 | let User = createUser() 92 | palisade(User, rules) 93 | User.authorized('read', { 94 | role: 'pleb' 95 | }).should.equal(false) 96 | }) 97 | it('should return false if user roles attr doesnt match', () => { 98 | let rules = { 99 | document: { 100 | read: [ 'admin' ] 101 | } 102 | } 103 | let User = createUser() 104 | palisade(User, rules) 105 | User.authorized('read', { 106 | roles: [ 'pleb' ] 107 | }).should.equal(false) 108 | }) 109 | it('should return true if user role attr matches', () => { 110 | let rules = { 111 | document: { 112 | read: [ 'admin', 'pleb' ] 113 | } 114 | } 115 | let User = createUser() 116 | palisade(User, rules) 117 | User.authorized('read', { 118 | role: 'admin' 119 | }).should.equal(true) 120 | }) 121 | it('should return true if user roles attr matches', () => { 122 | let rules = { 123 | document: { 124 | read: [ 'admin', 'super' ] 125 | } 126 | } 127 | let User = createUser() 128 | palisade(User, rules) 129 | User.authorized('read', { 130 | roles: [ 'pleb', 'admin' ] 131 | }).should.equal(true) 132 | }) 133 | it('should return true if user roles attr fn matches', () => { 134 | const isAdmin = ({ user, data }) => { 135 | should.exist(user) 136 | should.exist(user.roles) 137 | should.exist(data) 138 | data.id.should.equal(123) 139 | return true 140 | } 141 | let rules = { 142 | document: { 143 | read: [ isAdmin, 'super' ] 144 | } 145 | } 146 | let User = createUser() 147 | palisade(User, rules) 148 | User.authorized('read', { 149 | roles: [ 'pleb' ] 150 | }, { id: 123 }).should.equal(true) 151 | }) 152 | it('should return true if user roles attr fn doesnt match', () => { 153 | const isAdmin = ({ user, data }) => { 154 | should.exist(user) 155 | should.exist(user.roles) 156 | should.exist(data) 157 | data.id.should.equal(123) 158 | return false 159 | } 160 | let rules = { 161 | document: { 162 | read: [ isAdmin, 'super' ] 163 | } 164 | } 165 | let User = createUser() 166 | palisade(User, rules) 167 | User.authorized('read', { 168 | roles: [ 'pleb' ] 169 | }, { id: 123 }).should.equal(false) 170 | }) 171 | it('should return true if self role matches', () => { 172 | let rules = { 173 | document: { 174 | read: [ 'self' ] 175 | } 176 | } 177 | let User = createUser() 178 | palisade(User, rules) 179 | let u1 = new User({ id: '123' }) 180 | User.authorized('read', u1, u1).should.equal(true) 181 | u1.authorized('read', u1).should.equal(true) 182 | }) 183 | }) 184 | 185 | describe('Model.screen', () => { 186 | it('should exist on class', () => { 187 | let rules = {} 188 | let User = createUser() 189 | palisade(User, rules) 190 | should.exist(User.screen) 191 | }) 192 | it('should exist on instance', () => { 193 | let rules = {} 194 | let User = createUser() 195 | palisade(User, rules) 196 | should.exist(new User({}).screen) 197 | }) 198 | it('should return empty if no read rules specified', () => { 199 | let rules = {} 200 | let User = createUser() 201 | palisade(User, rules) 202 | User.screen('read', null, { name: 'test' }).should.eql({}) 203 | }) 204 | it('should return empty if no document rules for type specified', () => { 205 | let rules = { 206 | read: {} 207 | } 208 | let User = createUser() 209 | palisade(User, rules) 210 | User.screen('read', null, { name: 'test' }).should.eql({}) 211 | }) 212 | it('should return empty if document rules specified have no roles', () => { 213 | let rules = { 214 | read: { 215 | name: [] 216 | } 217 | } 218 | let User = createUser() 219 | palisade(User, rules) 220 | User.screen('read', null, { name: 'test' }).should.eql({}) 221 | }) 222 | it('should return data if read rules are public and no user given', () => { 223 | let rules = { 224 | read: { 225 | name: [ 'public' ] 226 | } 227 | } 228 | let User = createUser() 229 | let o = { name: 'test' } 230 | palisade(User, rules) 231 | User.screen('read', null, o).should.eql(o) 232 | }) 233 | it('should return empty if user role attr doesnt match', () => { 234 | let rules = { 235 | read: { 236 | name: [ 'admin' ] 237 | } 238 | } 239 | let User = createUser() 240 | let u1 = { role: 'pleb' } 241 | let o = { name: 'test' } 242 | palisade(User, rules) 243 | User.screen('read', u1, o).should.eql({}) 244 | }) 245 | it('should return empty if user roles attr doesnt match', () => { 246 | let rules = { 247 | read: { 248 | name: [ 'admin' ] 249 | } 250 | } 251 | let User = createUser() 252 | let u1 = { roles: [ 'pleb' ] } 253 | let o = { name: 'test' } 254 | palisade(User, rules) 255 | User.screen('read', u1, o).should.eql({}) 256 | }) 257 | it('should return data if user role attr matches', () => { 258 | let rules = { 259 | read: { 260 | name: [ 'admin', 'pleb' ] 261 | } 262 | } 263 | let User = createUser() 264 | let u1 = { role: 'admin' } 265 | let o = { name: 'test' } 266 | palisade(User, rules) 267 | User.screen('read', u1, o).should.eql(o) 268 | }) 269 | it('should return array data if user role attr matches', () => { 270 | let rules = { 271 | read: { 272 | name: [ 'admin', 'pleb' ] 273 | } 274 | } 275 | let User = createUser() 276 | let u1 = { role: 'admin' } 277 | let o = { name: 'test' } 278 | let data = [ o, o ] 279 | palisade(User, rules) 280 | User.screen('read', u1, data).should.eql(data) 281 | }) 282 | it('should return data if user roles attr matches', () => { 283 | let rules = { 284 | read: { 285 | name: [ 'admin', 'super' ] 286 | } 287 | } 288 | let User = createUser() 289 | let u1 = { roles: [ 'admin', 'pleb' ] } 290 | let o = { name: 'test' } 291 | palisade(User, rules) 292 | User.screen('read', u1, o).should.eql(o) 293 | }) 294 | it.skip('should return data if self role matches', () => { 295 | let rules = { 296 | read: { 297 | id: [ 'self' ], 298 | name: [ 'self' ] 299 | } 300 | } 301 | let User = createUser() 302 | palisade(User, rules) 303 | 304 | let o = { id: '123', name: 'test' } 305 | let u1 = new User(o) 306 | User.screen('read', u1, u1).should.eql(o) 307 | u1.screen('read', u1).should.eql(o) 308 | }) 309 | it('should return nested data if user roles attr matches', () => { 310 | let rules = { 311 | read: { 312 | attributes: { 313 | name: [ 'admin', 'super' ] 314 | } 315 | } 316 | } 317 | let User = createUser() 318 | let u1 = { roles: [ 'admin', 'pleb' ] } 319 | let o = { attributes: { name: 'test' } } 320 | palisade(User, rules) 321 | User.screen('read', u1, o).should.eql(o) 322 | }) 323 | it('should return empty nested data if user roles attr doesnt match', () => { 324 | let rules = { 325 | read: { 326 | attributes: { 327 | name: [ 'root', 'super' ] 328 | } 329 | } 330 | } 331 | let User = createUser() 332 | let u1 = { roles: [ 'admin', 'pleb' ] } 333 | let o = { attributes: { name: 'test' } } 334 | palisade(User, rules) 335 | User.screen('read', u1, o).should.eql({ attributes: {} }) 336 | }) 337 | it('should return nested data if user roles attr fn matches', () => { 338 | const yo = () => true 339 | let rules = { 340 | read: { 341 | attributes: { 342 | name: [ yo, 'root', 'super' ] 343 | } 344 | } 345 | } 346 | let User = createUser() 347 | let u1 = { roles: [ 'admin', 'pleb' ] } 348 | let o = { attributes: { name: 'test' } } 349 | palisade(User, rules) 350 | User.screen('read', u1, o).should.eql({ attributes: { name: 'test' } }) 351 | }) 352 | it('should return empty nested data if user roles attr fn doesnt match', () => { 353 | const yo = () => false 354 | let rules = { 355 | read: { 356 | attributes: { 357 | name: [ yo ] 358 | } 359 | } 360 | } 361 | let User = createUser() 362 | let u1 = { roles: [ 'admin', 'pleb' ] } 363 | let o = { attributes: { name: 'test' } } 364 | palisade(User, rules) 365 | User.screen('read', u1, o).should.eql({ attributes: {} }) 366 | }) 367 | }) 368 | 369 | describe('screenDeep', () => { 370 | it('should return an empty array when no read specified', () => { 371 | let User = createUser() 372 | palisade(User, {}) 373 | let u1 = new User({ 374 | id: '123', 375 | name: 'test1' 376 | }) 377 | let u2 = new User({ 378 | id: '456', 379 | name: 'test2' 380 | }) 381 | let data = [ u1, u2 ] 382 | screenDeep(null, data).should.eql([]) 383 | }) 384 | it('should return an empty nested array when no read specified', () => { 385 | let User = createUser() 386 | palisade(User, {}) 387 | let u1 = new User({ 388 | id: '123', 389 | name: 'test1' 390 | }) 391 | let u2 = new User({ 392 | id: '456', 393 | name: 'test2' 394 | }) 395 | let data = [ [ u1, u2 ] ] 396 | screenDeep(null, data).should.eql([ [ ] ]) 397 | }) 398 | it('should return an empty object when no read specified', () => { 399 | let User = createUser() 400 | palisade(User, {}) 401 | let u1 = new User({ 402 | id: '123', 403 | name: 'test1' 404 | }) 405 | let u2 = new User({ 406 | id: '456', 407 | name: 'test2' 408 | }) 409 | let data = { a: u1, b: u2 } 410 | screenDeep(null, data).should.eql({}) 411 | }) 412 | it('should return an empty nested object when no read specified', () => { 413 | let User = createUser() 414 | palisade(User, {}) 415 | let u1 = new User({ 416 | id: '123', 417 | name: 'test1' 418 | }) 419 | let u2 = new User({ 420 | id: '456', 421 | name: 'test2' 422 | }) 423 | let data = { c: { a: u1, b: u2 } } 424 | screenDeep(null, data).should.eql({ c: {} }) 425 | }) 426 | it('should return data when read public specified', () => { 427 | let User = createUser() 428 | palisade(User, { 429 | document: { 430 | read: [ 'public' ] 431 | }, 432 | read: { 433 | id: [ 'public' ], 434 | name: [ 'self' ] 435 | } 436 | }) 437 | let u1 = new User({ 438 | id: '123', 439 | name: 'test1' 440 | }) 441 | let u2 = new User({ 442 | id: '456', 443 | name: 'test2' 444 | }) 445 | screenDeep(u1, u2).should.eql({ id: '456' }) 446 | }) 447 | it('should return generic data', () => { 448 | let User = createUser() 449 | palisade(User, { 450 | document: { 451 | read: [ 'public' ] 452 | }, 453 | read: { 454 | id: [ 'public' ], 455 | time: [ 'public' ], 456 | name: [ 'self' ] 457 | } 458 | }) 459 | let u1 = new User({ 460 | id: '123', 461 | name: 'test1' 462 | }) 463 | let u2 = { 464 | id: '456', 465 | bday: new Date('2016-02-14T23:31:46.733Z'), 466 | name: 'test2' 467 | } 468 | screenDeep(u1, u2).should.eql({ 469 | id: '456', 470 | name: 'test2', 471 | bday: '2016-02-14T23:31:46.733Z' 472 | }) 473 | }) 474 | }) 475 | --------------------------------------------------------------------------------