├── .eslintrc ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── gulpfile.babel.js ├── package.json ├── resources ├── a-a-h.svg ├── a_a_h.png └── npm-prepublish.sh └── src ├── __tests__ └── index-test.js ├── core ├── __tests__ │ ├── authograph-test.js │ ├── bounds-test.js │ └── testSchema-test.js ├── authograph.js └── bounds.js └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "plugins": [ 5 | "babel" 6 | ], 7 | 8 | "env": { 9 | "es6": true, 10 | "node": true 11 | }, 12 | 13 | "ecmaFeatures": { 14 | "arrowFunctions": true, 15 | "binaryLiterals": true, 16 | "blockBindings": true, 17 | "classes": true, 18 | "defaultParams": true, 19 | "destructuring": true, 20 | "experimentalObjectRestSpread": true, 21 | "forOf": true, 22 | "generators": true, 23 | "globalReturn": true, 24 | "jsx": true, 25 | "modules": true, 26 | "objectLiteralComputedProperties": true, 27 | "objectLiteralDuplicateProperties": true, 28 | "objectLiteralShorthandMethods": true, 29 | "objectLiteralShorthandProperties": true, 30 | "octalLiterals": true, 31 | "regexUFlag": true, 32 | "regexYFlag": true, 33 | "restParams": true, 34 | "spread": true, 35 | "superInFunctions": true, 36 | "templateStrings": true, 37 | "unicodeCodePointEscapes": true 38 | }, 39 | 40 | "rules": { 41 | "babel/arrow-parens": [2, "as-needed"], 42 | "babel/array-bracket-spacing": [2, "always"], 43 | "babel/generator-star-spacing": [2, {"before": true, "after": false}], 44 | "array-bracket-spacing": 0, 45 | "generator-star-spacing": 0, 46 | 47 | "arrow-spacing": 2, 48 | "block-scoped-var": 0, 49 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 50 | "callback-return": 2, 51 | "camelcase": [2, {"properties": "always"}], 52 | "comma-dangle": 0, 53 | "comma-spacing": 0, 54 | "comma-style": [2, "last"], 55 | "complexity": 0, 56 | "computed-property-spacing": [2, "never"], 57 | "consistent-return": 0, 58 | "consistent-this": 0, 59 | "curly": [2, "all"], 60 | "default-case": 0, 61 | "dot-location": [2, "property"], 62 | "dot-notation": 0, 63 | "eol-last": 2, 64 | "eqeqeq": 2, 65 | "func-names": 0, 66 | "func-style": 0, 67 | "guard-for-in": 2, 68 | "handle-callback-err": [2, "error"], 69 | "id-length": 0, 70 | "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"], 71 | "indent": [2, 2, {"SwitchCase": 1}], 72 | "init-declarations": 0, 73 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 74 | "keyword-spacing": 2, 75 | "linebreak-style": 2, 76 | "lines-around-comment": 0, 77 | "max-depth": 0, 78 | "max-len": [2, 80, 4], 79 | "max-nested-callbacks": 0, 80 | "max-params": 0, 81 | "max-statements": 0, 82 | "new-cap": 0, 83 | "new-parens": 2, 84 | "newline-after-var": 0, 85 | "no-alert": 2, 86 | "no-array-constructor": 2, 87 | "no-bitwise": 0, 88 | "no-caller": 2, 89 | "no-catch-shadow": 0, 90 | "no-class-assign": 2, 91 | "no-cond-assign": 2, 92 | "no-console": 0, 93 | "no-const-assign": 2, 94 | "no-constant-condition": 2, 95 | "no-continue": 0, 96 | "no-control-regex": 0, 97 | "no-debugger": 1, 98 | "no-delete-var": 2, 99 | "no-div-regex": 2, 100 | "no-dupe-args": 2, 101 | "no-dupe-keys": 2, 102 | "no-duplicate-case": 2, 103 | "no-else-return": 2, 104 | "no-empty": 2, 105 | "no-empty-character-class": 2, 106 | "no-eq-null": 0, 107 | "no-eval": 2, 108 | "no-ex-assign": 2, 109 | "no-extend-native": 2, 110 | "no-extra-bind": 2, 111 | "no-extra-boolean-cast": 2, 112 | "no-extra-parens": 0, 113 | "no-extra-semi": 2, 114 | "no-fallthrough": 2, 115 | "no-floating-decimal": 2, 116 | "no-func-assign": 2, 117 | "no-implicit-coercion": 2, 118 | "no-implied-eval": 2, 119 | "no-inline-comments": 0, 120 | "no-inner-declarations": [2, "functions"], 121 | "no-invalid-regexp": 2, 122 | "no-invalid-this": 0, 123 | "no-irregular-whitespace": 2, 124 | "no-iterator": 2, 125 | "no-label-var": 2, 126 | "no-labels": [2, {"allowLoop": true}], 127 | "no-lone-blocks": 2, 128 | "no-lonely-if": 2, 129 | "no-loop-func": 0, 130 | "no-mixed-requires": [2, true], 131 | "no-mixed-spaces-and-tabs": 2, 132 | "no-multi-spaces": 2, 133 | "no-multi-str": 2, 134 | "no-multiple-empty-lines": 0, 135 | "no-native-reassign": 0, 136 | "no-negated-in-lhs": 2, 137 | "no-nested-ternary": 0, 138 | "no-new": 2, 139 | "no-new-func": 0, 140 | "no-new-object": 2, 141 | "no-new-require": 2, 142 | "no-new-wrappers": 2, 143 | "no-obj-calls": 2, 144 | "no-octal": 2, 145 | "no-octal-escape": 2, 146 | "no-param-reassign": 2, 147 | "no-path-concat": 2, 148 | "no-plusplus": 0, 149 | "no-process-env": 0, 150 | "no-process-exit": 0, 151 | "no-proto": 2, 152 | "no-redeclare": 2, 153 | "no-regex-spaces": 2, 154 | "no-restricted-modules": 0, 155 | "no-return-assign": 2, 156 | "no-script-url": 2, 157 | "no-self-compare": 0, 158 | "no-sequences": 2, 159 | "no-shadow": 2, 160 | "no-shadow-restricted-names": 2, 161 | "no-spaced-func": 2, 162 | "no-sparse-arrays": 2, 163 | "no-sync": 2, 164 | "no-ternary": 0, 165 | "no-this-before-super": 2, 166 | "no-throw-literal": 2, 167 | "no-trailing-spaces": 2, 168 | "no-undef": 2, 169 | "no-undef-init": 2, 170 | "no-undefined": 0, 171 | "no-underscore-dangle": 0, 172 | "no-unexpected-multiline": 2, 173 | "no-unneeded-ternary": 2, 174 | "no-unreachable": 2, 175 | "no-unused-expressions": 2, 176 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 177 | "no-use-before-define": 0, 178 | "no-useless-call": 2, 179 | "no-var": 2, 180 | "no-void": 2, 181 | "no-warning-comments": 0, 182 | "no-with": 2, 183 | "object-curly-spacing": [0, "always"], 184 | "object-shorthand": [2, "always"], 185 | "one-var": [2, "never"], 186 | "operator-assignment": [2, "always"], 187 | "operator-linebreak": [2, "after"], 188 | "padded-blocks": 0, 189 | "prefer-const": 2, 190 | "prefer-reflect": 0, 191 | "prefer-spread": 0, 192 | "quote-props": [2, "as-needed"], 193 | "quotes": [2, "single"], 194 | "radix": 2, 195 | "require-yield": 2, 196 | "semi": [2, "always"], 197 | "semi-spacing": [2, {"before": false, "after": true}], 198 | "sort-vars": 0, 199 | "space-before-blocks": [2, "always"], 200 | "space-before-function-paren": [2, {"anonymous": "always", "named": "never"}], 201 | "space-in-parens": 0, 202 | "space-infix-ops": [2, {"int32Hint": false}], 203 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 204 | "spaced-comment": [2, "always"], 205 | "strict": 0, 206 | "use-isnan": 2, 207 | "valid-jsdoc": 0, 208 | "valid-typeof": 2, 209 | "vars-on-top": 0, 210 | "wrap-iife": 2, 211 | "wrap-regex": 0, 212 | "yoda": [2, "never", {"exceptRange": true}] 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * -crlf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | 9 | dist 10 | node_modules 11 | coverage 12 | .nyc_output 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.swp 3 | *~ 4 | *.iml 5 | .*.haste_cache.* 6 | .DS_Store 7 | .idea 8 | npm-debug.log 9 | 10 | CONTRIBUTING.md 11 | node_modules 12 | coverage 13 | resources 14 | src 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.1" 4 | after_script: 5 | - YOURPACKAGE_COVERAGE=1 npm run lcov | ./node_modules/.bin/coveralls -v 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For Authograph software 4 | 5 | Copyright (c) 2016, Benjamin Baldivia All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Benjamin Baldivia nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | Status API Training Shop Blog About 32 | © 2016 GitHub, Inc. Terms Privacy Security Contact Help 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Authograph 2 | ========== 3 | ##### Authorization solution for use with GraphQL. 4 | [![npm version](https://badge.fury.io/js/authograph.svg)](https://badge.fury.io/js/authograph) [![Build Status](https://travis-ci.org/Goblinlordx/authograph.svg?branch=master)](https://travis-ci.org/Goblinlordx/authograph) [![Coverage Status](https://coveralls.io/repos/github/Goblinlordx/authograph/badge.svg?branch=master)](https://coveralls.io/github/Goblinlordx/authograph?branch=master) 5 | 6 | Authograph is intended to be a solution specifically for handling Authorization. Authograph is an implementation of [Role Based Access Control](https://en.wikipedia.org/wiki/Role-based_access_control). On any given server Access Control can broken down into 2 phases prior passing an event to some handler function. 7 | 8 | ![alt text](https://raw.githubusercontent.com/Goblinlordx/authograph/master/resources/a_a_h.png "Authentication -> Authorization -> Request Handler (REST, GraphQL, etc.)") 9 | 10 | 11 | 12 | Authentication should occur first. This is the act of identifying a specific user. This can be done in many ways. You might use Passport.js middleware to accomplish this task. You could also use the phase of the moon if you really wanted as Authograph has no requirement as far as how you accomplish this. 13 | 14 | Authorization is done via Authograph and is Authographs core functionality. Currently, Authograph is being developed for use specifically with GraphQL schemas. Authograph will whitelist a schema based on a permission set as well as wrap resolving functions with bounding functions to bound client sent arguments based on role. 15 | 16 | Lastly, after Authograph has processed the base schema the resultant schema can then be used by a GraphQL handler (graphql, express-graphql, etc). 17 | 18 | 19 | ## Getting Started 20 | 21 | Install for use in your project 22 | 23 | ```sh 24 | npm i -S authograph 25 | ``` 26 | 27 | #### Usage 28 | There are 2 main ways to use this package. You can either use a configured instance as middleware or you can use the standalone filterSchema function. 29 | 30 | ##### Using as middleware 31 | The ```.middleware(schema)``` method on a configured instance as middleware. This will attach a ```.schema``` property to the ```req``` object. 32 | ``` 33 | getRoles: 34 | builtPSet: 35 | ``` 36 | 37 | ```js 38 | import Authograph from 'authograph'; 39 | 40 | const instance = new Authograph({ 41 | getRoles(req) { 42 | console.log("getRoles called"); 43 | var user = req.user||{}; 44 | if(!(user.Roles instanceof Array)) 45 | return Promise.resolve([]); 46 | return Promise.resolve(user.Roles.map(o => o.type)); 47 | }, 48 | buildPSet(roleIds) { 49 | Promise.resolve({ 50 | Query: { 51 | users: { 52 | id: { 53 | admin:{ 54 | min: 1, 55 | max: 6 56 | } 57 | }, 58 | name: { 59 | admin:{} 60 | }, 61 | email: { 62 | admin:{} 63 | } 64 | } 65 | } 66 | }); 67 | } 68 | }); 69 | export default instance; 70 | ``` 71 | 72 | ##### getRoles() 73 | ```getRoles``` must return a promise. The promise itself can resolve to any type which is serializable. The serialization of the roles will be used for keeping a hash to map to a previously resolved schema definition with permissions applied. In general, it can be any set of things as the hashing function can also be overwritten in the config. 74 | 75 | ##### buildPSet() 76 | ```buildPSet``` must return a promise. The promise should resolve to a Object with the following format. 77 | ```js 78 | : { 79 | : { 80 | : { 81 | : { 82 | : // Optional 83 | } 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | ##### Using filterSchema 90 | The ```filterSchema``` function is provided if you would like to manage the request to permissions mapping. This function can be used to simply take a base schema, a permissions object, and a bounding function definition object. ```filterSchema``` will output a sanitized schema which can then be used by GraphQL. 91 | 92 | ```js 93 | import {filterSchema} from 'authograph'; 94 | import {graphql} from 'graphql'; 95 | import mySchema from './mySchema'; 96 | import myBounds from './myBounds'; 97 | 98 | . 99 | . 100 | . 101 | 102 | const processQuery = async (query, permissionsObj) => { 103 | const restrictedSchema = filterSchema(mySchema, permissionsObj, myBounds); 104 | return await graphql(restrictedSchema, query); 105 | }; 106 | ``` 107 | -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import shell from 'gulp-shell'; 3 | 4 | gulp.task('watch', () => { 5 | gulp.watch('src/**/*.js', [ 'build' ]); 6 | }); 7 | 8 | gulp.task('watch-report', () => { 9 | gulp.watch('src/**/*.js', ['lcov']); 10 | }); 11 | 12 | gulp.task('lcov', shell.task([ 'npm run test-report' ])); 13 | 14 | gulp.task('build', shell.task([ 'npm test' ])); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authograph", 3 | "version": "0.0.1n", 4 | "description": "Authorization solution for use with GraphQL", 5 | "contributors": [ 6 | "Benjamin Baldivia " 7 | ], 8 | "license": "BSD-3-Clause", 9 | "main": "index.js", 10 | "babel": { 11 | "presets": [ 12 | "es2015" 13 | ], 14 | "plugins": [ 15 | "syntax-async-functions", 16 | "transform-class-properties", 17 | "transform-flow-strip-types", 18 | "transform-object-rest-spread", 19 | "transform-regenerator", 20 | "transform-runtime" 21 | ] 22 | }, 23 | "nyc": { 24 | "include": [ 25 | "src/**" 26 | ] 27 | }, 28 | "scripts": { 29 | "test": "npm run lint && npm run cover", 30 | "test-report": "npm run lint && npm run creport", 31 | "testonly": "mocha --compilers js:babel-core/register --check-leaks --full-trace src/**/__tests__/**/*-test.js", 32 | "cover": "nyc --all --require babel-core/register mocha src/**/__tests__/**/*-test.js||true", 33 | "creport": "nyc --all --reporter=lcov --require babel-core/register mocha src/**/__tests__/**/*-test.js||true", 34 | "lcov": "nyc report --reporter=text-lcov||true", 35 | "lint": "eslint src||true", 36 | "watch": "gulp watch", 37 | "watch-report": "gulp watch-report", 38 | "build": "babel src --ignore __tests__ --out-dir dist/", 39 | "prepub": "rm -rf ./dist && npm run build && cp README.md ./dist && cp package.json ./dist" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/goblinlordx/authograph.git" 44 | }, 45 | "keywords": [ 46 | "graphql", 47 | "rbac", 48 | "authorization", 49 | "security" 50 | ], 51 | "bugs": { 52 | "url": "https://github.com/goblinlordx/authograph/issues" 53 | }, 54 | "homepage": "https://github.com/goblinlordx/authograph#readme", 55 | "dependencies": { 56 | "lodash": "^4.9.0" 57 | }, 58 | "devDependencies": { 59 | "babel-cli": "6.6.5", 60 | "babel-eslint": "6.0.2", 61 | "babel-plugin-syntax-async-functions": "6.5.0", 62 | "babel-plugin-transform-class-properties": "6.6.0", 63 | "babel-plugin-transform-flow-strip-types": "6.7.0", 64 | "babel-plugin-transform-object-rest-spread": "6.6.5", 65 | "babel-plugin-transform-regenerator": "6.6.5", 66 | "babel-plugin-transform-runtime": "6.6.0", 67 | "babel-preset-es2015": "6.6.0", 68 | "chai": "^3.5.0", 69 | "coveralls": "^2.11.9", 70 | "eslint": "^2.8.0", 71 | "eslint-plugin-babel": "^3.2.0", 72 | "graphql": "^0.5.0", 73 | "gulp": "^3.9.1", 74 | "gulp-shell": "^0.5.2", 75 | "mocha": "^2.4.5", 76 | "nyc": "^6.4.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /resources/a-a-h.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | G 4 | 5 | 6 | Authentication 7 | 8 | Authentication 9 | 10 | 11 | Authorization 12 | 13 | Authorization 14 | 15 | 16 | Authentication->Authorization 17 | 18 | 19 | 20 | 21 | Request handler\n(REST, GraphQL, etc) 22 | 23 | Request handler 24 | (REST, GraphQL, etc) 25 | 26 | 27 | Authorization->Request handler\n(REST, GraphQL, etc) 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /resources/a_a_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Goblinlordx/authograph/bfb636fd8e11ec6cc28b5e40711a80a4de572dc5/resources/a_a_h.png -------------------------------------------------------------------------------- /resources/npm-prepublish.sh: -------------------------------------------------------------------------------- 1 | # Because of a long-running npm issue (https://github.com/npm/npm/issues/3059) 2 | # prepublish runs after `npm install` and `npm pack`. 3 | # In order to only run prepublish before `npm publish`, we have to check argv. 4 | if node -e "process.exit(($npm_config_argv).original[0].indexOf('pu') === 0)"; then 5 | exit 0; 6 | fi 7 | 8 | # Publishing to NPM is currently supported by Travis CI, which ensures that all 9 | # tests pass first and the deployed module contains the correct file structure. 10 | # In order to prevent inadvertently circumventing this, we ensure that a CI 11 | # environment exists before continuing. 12 | if [ "$CI" != true ]; then 13 | echo "\n\n\n \033[101;30m Only Travis CI can publish to NPM. \033[0m" 1>&2; 14 | echo " Ensure git is left is a good state by backing out any commits and deleting any tags." 1>&2; 15 | echo " Then read CONTRIBUTING.md to learn how to publish to NPM.\n\n\n" 1>&2; 16 | exit 1; 17 | fi; 18 | 19 | # When Travis CI publishes to NPM, the published files are available in the root 20 | # directory, which allows for a clean include or require of sub-modules. 21 | # 22 | # var language = require('graphql/language'); 23 | # 24 | npm run build 25 | 26 | # Ensure a vanilla package.json before deploying so other tools do not interpret 27 | # The built output as requiring any further transformation. 28 | node -e "var package = require('./package.json'); \ 29 | delete package.babel; delete package.scripts; delete package.options; \ 30 | require('fs').writeFileSync('package.json', JSON.stringify(package));" 31 | -------------------------------------------------------------------------------- /src/__tests__/index-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016, Ben Baldivia 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import {filterSchema} from '../index'; 11 | import Authograph from '../index'; 12 | import {describe, it} from 'mocha'; 13 | import {expect} from 'chai'; 14 | 15 | describe('Index', () => { 16 | it('exports Authograph by default', () => { 17 | expect(Authograph.name).to.equal('Authograph'); 18 | expect(Authograph).to.not.equal(undefined); 19 | }); 20 | it('exports filterSchema()', () => { 21 | expect(filterSchema).to.not.equal(undefined); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/core/__tests__/authograph-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016, Ben Baldivia 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import { 11 | filterSchema, 12 | Authograph 13 | } from '../authograph'; 14 | 15 | import { 16 | testSchema, 17 | testPermissions 18 | } from './testSchema-test.js'; 19 | 20 | import { 21 | GraphQLSchema 22 | } from 'graphql'; 23 | 24 | import {describe, it} from 'mocha'; 25 | import {expect} from 'chai'; 26 | 27 | describe('Authograph', () => { 28 | it('exports Authograph', () => { 29 | expect(Authograph).to.not.equal(undefined); 30 | }); 31 | it('exports filterSchema()', () => { 32 | expect(filterSchema).to.not.equal(undefined); 33 | }); 34 | it('is extendable', () => { 35 | const authObj = new Authograph(); 36 | expect(authObj).to.be.instanceof(Authograph); 37 | }); 38 | describe('.getRoles()', () => { 39 | it('Returns an Promise resolving empty Array by default', async () => { 40 | const authObj = new Authograph(); 41 | const result = authObj.getRoles(); 42 | expect(result).to.be.instanceof(Promise); 43 | const pResult = await result; 44 | expect(pResult).to.be.instanceof(Array); 45 | expect(pResult.length).to.be.equal(0); 46 | }); 47 | }); 48 | describe('.buildPSet()', () => { 49 | it('Returns an Promise resolving empty Object by default', async () => { 50 | const authObj = new Authograph(); 51 | const result = authObj.buildPSet(); 52 | expect(result).to.be.instanceof(Promise); 53 | const pResult = await result; 54 | expect(pResult).to.be.instanceof(Object); 55 | expect(Object.keys(pResult).length).to.be.equal(0); 56 | }); 57 | }); 58 | }); 59 | describe('filterSchema()', () => { 60 | it('when passed empty permissions will return undefined', () => { 61 | const result = filterSchema(testSchema, {}); 62 | expect(result).to.equal(undefined); 63 | }); 64 | it('when passed permissions will return schema', () => { 65 | const result = filterSchema(testSchema, testPermissions); 66 | expect(result).to.be.instanceof(GraphQLSchema); 67 | }); 68 | it('when passed permissions properly filters unauthorized fields', () => { 69 | const result = filterSchema(testSchema, testPermissions); 70 | expect(result._typeMap.Query._fields.name).to.equal(undefined); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/core/__tests__/bounds-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016, Ben Baldivia 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import { 11 | min, 12 | max, 13 | oneOf 14 | } from '../bounds'; 15 | 16 | import {describe, it} from 'mocha'; 17 | import {expect} from 'chai'; 18 | 19 | 20 | describe('Bounding function tests:', () => { 21 | describe('min',() => { 22 | it('passes when argument is not received', () => { 23 | expect(min('testArg', 2, null, {})).to.equal(true); 24 | }); 25 | it('passes when argument equal to min', () => { 26 | expect(min('testArg', 2, null, {testArg: 2})).to.equal(true); 27 | }); 28 | it('passes when argument greater than min', () => { 29 | expect(min('testArg', 2, null, {testArg: 3})).to.equal(true); 30 | }); 31 | it('fails when argument less than min', () => { 32 | expect(min('testArg', 2, null, {testArg: 1})).to.equal(false); 33 | }); 34 | }); 35 | describe('max', () => { 36 | it('passes when argument is not received', () => { 37 | expect(max('testArg', 2, null, {})).to.equal(true); 38 | }); 39 | it('passes when argument equal to max', () => { 40 | expect(max('testArg', 2, null, {testArg: 2})).to.equal(true); 41 | }); 42 | it('fails when argument greater than max', () => { 43 | expect(max('testArg', 2, null, {testArg: 3})).to.equal(false); 44 | }); 45 | it('passes when argument less than max', () => { 46 | expect(max('testArg', 2, null, {testArg: 1})).to.equal(true); 47 | }); 48 | }); 49 | describe('oneOf', () => { 50 | it('passes when argument is not received', () => { 51 | expect(oneOf('testArg', 2, null, {})).to.equal(true); 52 | }); 53 | it('passes when argument value is in set', () => { 54 | expect(oneOf('testArg', [ 'test', 'asdf' ], 55 | null, {testArg: 'asdf'})).to.equal(true); 56 | }); 57 | it('passes when argument value is single value and not array', () => { 58 | expect(oneOf('testArg', 'asdf', 59 | null, {testArg: 'asdf'})).to.equal(true); 60 | }); 61 | it('fails when argument is not in set', () => { 62 | expect(oneOf('testArg', [ 'test', 'asdf' ], 63 | null, {testArg: 'fail'})).to.equal(false); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/core/__tests__/testSchema-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016, Ben Baldivia 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import { 11 | GraphQLSchema, 12 | GraphQLObjectType, 13 | GraphQLList, 14 | GraphQLString, 15 | GraphQLInt 16 | } from 'graphql'; 17 | 18 | export const testPermissions = { 19 | Query: { 20 | id: { 21 | _: { 22 | admin: {} 23 | } 24 | }, 25 | list: { 26 | _: { 27 | admin: {} 28 | } 29 | } 30 | } 31 | }; 32 | 33 | export const Query = new GraphQLObjectType({ 34 | name: 'Query', 35 | desciption: 'Test Query Type;', 36 | fields() { 37 | return { 38 | id: { 39 | type: GraphQLInt, 40 | resolve() { 41 | return 1; 42 | } 43 | }, 44 | name: { 45 | type: GraphQLString, 46 | resolve() { 47 | return 'Test Usera Name'; 48 | } 49 | }, 50 | list: { 51 | type: new GraphQLList(GraphQLString), 52 | resolve() { 53 | return [ 'List item 1', 'List item 2' ]; 54 | } 55 | } 56 | }; 57 | } 58 | }); 59 | 60 | export const testSchema = new GraphQLSchema({ 61 | query: Query 62 | }); 63 | -------------------------------------------------------------------------------- /src/core/authograph.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016, Ben Baldivia 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | import _ from 'lodash'; 10 | import * as Bounds from './bounds'; 11 | 12 | const GraphQLTypes = [ 13 | 'Int', 14 | 'String', 15 | '__Schema', 16 | '__Type', 17 | '__TypeKind', 18 | 'Boolean', 19 | '__Field', 20 | '__InputValue', 21 | '__EnumValue', 22 | '__Directive' 23 | ]; 24 | 25 | 26 | /** 27 | * remapFieldType recurses through wrapped object types to find the base 28 | * Object Type. If the type is not in a whitelist of types (newTypeList), 29 | * The Type is returned undefined. If it is in the whitelist a new type 30 | * which has the prototype of the old type is returned. 31 | * @param {Object} schemaTypeMap - schema._typeMap from a base GraphQL schema 32 | * @param {Object} typeMap - the typeMap which will be used in new schema 33 | * @param {Object} type - Type which is being checked 34 | * @param {Array} newTypeList - List of Type.name which are whitelisted in 35 | * new schema 36 | */ 37 | const remapFieldType = (schemaTypeMap, typeMap, type, newTypeList) => { 38 | if (type.ofType) { 39 | const recurse = remapFieldType(schemaTypeMap, 40 | typeMap, type.ofType, newTypeList); 41 | if (recurse) { 42 | const container = Object.create(type); 43 | container.ofType = recurse; 44 | return container; 45 | } 46 | return; 47 | } 48 | const ftk = type.name; 49 | if (newTypeList.indexOf(ftk) === -1) { 50 | return; 51 | } 52 | if (!typeMap[ftk]) { 53 | typeMap[ftk] = Object.create(schemaTypeMap[ftk]); 54 | } 55 | return typeMap[ftk]; 56 | }; 57 | 58 | /** 59 | * bounder function wraps a resolver with a function which validates bounding 60 | * functions. 61 | * @param {Function} resolve - GraphQL field resolve function 62 | * @param {Object} bounds - Object containing bounds 63 | * @param {Object} BoundingDefs - Object containing bounding 64 | * function definitions 65 | * @return {Function} resolver - Returns a GraphQL field resolve function 66 | */ 67 | const bounder = (resolve, bounds, BoundingDefs) => { 68 | return (root, args, context) => { 69 | const permittedRoles = context.schema._permittedRoles || []; 70 | const oob = context.schema._oob; 71 | 72 | Object.keys(bounds) 73 | .forEach(arg => { 74 | return Object.keys(bounds[arg]) 75 | .forEach(role => { 76 | if (permittedRoles.indexOf(role) === -1) { 77 | return; 78 | } 79 | const pass = Object.keys(bounds[arg][role]) 80 | .every(bound => { 81 | if (!BoundingDefs[bound]) { 82 | console.warn(new Error(`Invalid permission bounder ${bound}`)); 83 | return true; 84 | } 85 | return BoundingDefs[bound](arg, 86 | bounds[arg][role][bound], root, args, context); 87 | }); 88 | if (!pass) { 89 | console.log(context); 90 | permittedRoles.splice(permittedRoles.indexOf(role),1); 91 | oob.push([ 92 | context.parentType.name, 93 | context.fieldName, 94 | bounds, 95 | args 96 | ]); 97 | } 98 | }); 99 | }); 100 | if (permittedRoles.length === 0) { 101 | throw new Error(`Parameters exceeded bounds: ${JSON.stringify(oob)}}`); 102 | } 103 | return resolve(root, args, context); 104 | }; 105 | }; 106 | 107 | /** 108 | * filterSchema takes as parameters a base GraphQL schema, permissions object, 109 | * and a list of permitted roles that can use the resultant schema. The return 110 | * is the resultant schema which has been sanitized and has bounding functions 111 | * injected into the field resolver functions. 112 | * @param {GraphQLSchema} schema - Base GraphQL schema 113 | * @param {PSet} pSet - Permissions set object 114 | * @param {Object} BoundingDefs - Object containing bounding 115 | * function definitions 116 | * @param {Array} permittedRoles - Roles which are permitted to use resultant 117 | * schema. (Optional) 118 | * @return {GraphQLSchema} - Sanitized resultant schema 119 | */ 120 | export const filterSchema = (schema, pSet, BoundingDefs, permittedRoles) => { 121 | let deriveP; 122 | let pRoles; 123 | if (permittedRoles) { 124 | pRoles = permittedRoles.slice(0, permittedRoles.length); 125 | } else { 126 | deriveP = true; 127 | pRoles = []; 128 | } 129 | const permitTypes = _.intersection(Object.keys(pSet), 130 | Object.keys(schema._typeMap)); 131 | const newTypeList = GraphQLTypes.concat(permitTypes); 132 | const filteredSchema = Object.create(schema); 133 | 134 | filteredSchema._typeMap = permitTypes 135 | .reduce((typeMap, typeKey) => { 136 | let typeOverlay; 137 | if (typeMap[typeKey]) { 138 | typeOverlay = typeMap[typeKey]; 139 | } else { 140 | typeOverlay = Object.create(schema._typeMap[typeKey]); 141 | } 142 | 143 | typeOverlay._fields = _.intersection(Object.keys(pSet[typeKey]), 144 | Object.keys(typeOverlay._fields)) 145 | .reduce((fieldMap,fieldKey) => { 146 | const fieldOverlay = Object.create(typeOverlay._fields[fieldKey]); 147 | fieldOverlay.type = remapFieldType(schema._typeMap, typeMap, 148 | fieldOverlay.type, newTypeList); 149 | if (fieldOverlay.type) { 150 | fieldOverlay.args = fieldOverlay.args 151 | .filter(arg => Object.keys(pSet[typeKey][fieldKey]) 152 | .indexOf(arg.name) !== -1 || arg.name === '_'); 153 | 154 | fieldOverlay.resolve = bounder(fieldOverlay.resolve, 155 | pSet[typeKey][fieldKey], 156 | BoundingDefs); 157 | if (deriveP) { 158 | Object.keys(pSet[typeKey][fieldKey]).forEach(arg => { 159 | Object.keys(pSet[typeKey][fieldKey][arg]).forEach(role => { 160 | if (pRoles.indexOf(role) === -1) { 161 | pRoles.push(role); 162 | } 163 | }); 164 | }); 165 | } 166 | fieldMap[fieldKey] = fieldOverlay; 167 | } 168 | return fieldMap; 169 | },{}); 170 | typeMap[typeKey] = typeOverlay; 171 | return typeMap; 172 | }, GraphQLTypes 173 | .reduce((r,k) => { 174 | r[k] = schema._typeMap[k]; 175 | return r; 176 | },{})); 177 | 178 | if (!filteredSchema._typeMap[filteredSchema._queryType.name]) { 179 | return; // Invalid schema 180 | } 181 | filteredSchema._queryType = filteredSchema 182 | ._typeMap[filteredSchema._queryType.name]; 183 | 184 | if (filteredSchema._mutationType && 185 | filteredSchema._typeMap[filteredSchema._mutationType.name]) { 186 | 187 | filteredSchema._mutationType = filteredSchema 188 | ._typeMap[filteredSchema._mutationType.name]; 189 | 190 | } else { 191 | // Prevent property lookup of old mutation type 192 | filteredSchema._mutationType = undefined; 193 | } 194 | filteredSchema._permittedRoles = pRoles; 195 | return filteredSchema; 196 | }; 197 | 198 | export class Authograph { 199 | constructor(o = {}) { 200 | if (o.getRoles instanceof Function) { 201 | this.getRoles = o.getRoles; 202 | } 203 | if (o.buildPSet instanceof Function) { 204 | this.buildPSet = o.buildPSet; 205 | } 206 | if (o.hashRoles instanceof Function) { 207 | this.hashRoles = o.hashRoles; 208 | } 209 | if (o.emptySchemaHandler instanceof Function) { 210 | this.emptySchemaHandler = o.emptySchemaHandler; 211 | } 212 | if (o.cacheVersion instanceof Function) { 213 | this.cacheVersion = o.cacheVersion; 214 | } 215 | this.Bounds = _.assign({},Bounds, o.Bounds); 216 | this.caching = o.caching || false; 217 | this.schemaCache = {}; 218 | } 219 | 220 | cacheVersion() { 221 | return Promise.resolve(this._currentCacheVersion); 222 | } 223 | 224 | setCacheVersion(v) { 225 | this._currentCacheVersion = v; 226 | return Promise.resolve(v); 227 | } 228 | 229 | validateCache() { 230 | this.cacheVersion() 231 | .then(v => { 232 | if (v !== this.cacheVersion) { 233 | return false; 234 | } 235 | return true; 236 | }); 237 | } 238 | 239 | flushCache() { 240 | this.schemaCache = {}; 241 | return Promise.resolve(true); 242 | } 243 | 244 | getRoles() { 245 | return Promise.resolve([]); 246 | } 247 | 248 | buildPSet() { 249 | return Promise.resolve({}); 250 | } 251 | 252 | hashRoles(roles) { 253 | return roles.sort().join(); 254 | } 255 | 256 | httpErrorHandler(req, res, err) { 257 | res.status(500).send('Internal Server Error'); 258 | throw err; 259 | } 260 | 261 | middleware(baseSchema) { 262 | return (req, res, next) => { 263 | try { 264 | let roles; 265 | return this.getRoles(req) 266 | .then(r => { 267 | roles = r; 268 | return this.buildPSet(roles); 269 | }) 270 | .then(pSet => { 271 | return filterSchema(baseSchema, pSet, this.Bounds, roles); 272 | }) 273 | .then(schema => { 274 | if (schema) { 275 | req.schema = Object.create(schema); 276 | req.schema._oob = []; 277 | } else { 278 | console.log('Insufficient permissions to use schema'); 279 | if (this.emptySchemaHandler) { 280 | return this.emptySchemaHandler(req,res); 281 | } 282 | return res.status(404).send('Not found'); 283 | } 284 | next(); 285 | }); 286 | } catch (err) { 287 | return this.httpErrorHandler(req, res, err); 288 | } 289 | }; 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/core/bounds.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016, Ben Baldivia 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | export const min = (arg, boundary, root, args) => { 11 | if (!args[arg]) { 12 | return true; 13 | } 14 | return args[arg] >= boundary; 15 | }; 16 | 17 | export const max = (arg, boundary, root, args) => { 18 | if (!args[arg]) { 19 | return true; 20 | } 21 | return args[arg] <= boundary; 22 | }; 23 | 24 | export const oneOf = (arg, boundary, root, args) => { 25 | if (!args[arg]) { 26 | return true; 27 | } 28 | let b; 29 | if (!(boundary instanceof Array)) { 30 | b = [].concat(boundary); 31 | } else { 32 | b = boundary; 33 | } 34 | return b.indexOf(args[arg]) !== -1; 35 | }; 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2016, Ben Baldivia 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | import { 11 | filterSchema, 12 | Authograph 13 | } from './core/authograph'; 14 | export {filterSchema}; 15 | export default Authograph; 16 | --------------------------------------------------------------------------------