Commit d8262c92 authored by Vadym Gidulian's avatar Vadym Gidulian

Merge branch 'dev'

parents 1a2bc699 d25089ff
/.data
.env
# Dependency directories
node_modules/
# Logs
logs/
*.log
npm-debug.log*
# Optional npm cache directory
.npm/
# Optional REPL history
.node_repl_history/
*
!/src/**
# token-based-authz-middleware
An Express middleware for token-based authorization.
## Usage
```js
app.use(authzMiddleware(options))
```
### Options
- `headerName` `[string]` Default: `X-Token`
Header name used to pass a token.
- `pathToRules` `[string]`
Path to file containing tokens and rules associated with them.
### Rules
Rules are a JSON file containing a single object with tokens as keys and rules associated with them as values.
```
{
"token1": <tokenRules>,
"token2": <tokenRules>,
...
}
```
`<tokenRules>` may be one of the following:
- `boolean` - if `true` access is allowed, denied otherwise.
- `<tokenRule>` - an object, which may contain the following properties:
- `methods` - an array of allowed HTTP methods
- `paths` - an array of paths access to which is allowed. Path is a `RegExp` string.
_At least one property must be specified._
- `Array` - an array of `<tokenRule>`s. The resulting rule is a union of listed rules.
Tokens specified in rules are trimmed. Spaces around tokens should be avoided because it may lead to ambiguous behavior.
Empty string may be used to define rules for requests w/o token or w/ empty one.
#### Example
```json
{
"token1": false,
"token2": true,
"token3": {
"methods": ["get"]
},
"token4": {
"paths": ["^/admin"]
},
"token5": [
{
"methods": ["get"]
}, {
"methods": ["post"],
"paths": ["comment"]
}
],
"": {
"methods": ["get"]
}
}
```
- `token1` - All requests will be blocked.
- `token2` - All requests will pass.
- `token3` - Only `GET` requests will pass.
- `token4` - All requests to paths beginning with `admin` will pass.
- `token5` - Only `GET` requests or `POST` requests to paths containing `comment` will pass.
- All `GET` requests w/o token or w/ empty one will pass.
version: '3'
services:
server:
image: node:10-alpine
volumes:
- .:/app
working_dir: /app
entrypoint: sh
command: -c "yarn install --production=false && npx nodemon --signal SIGHUP -x 'npm test'"
networks:
default:
aliases:
- api
{
"name": "@gviagroup/token-based-authz-middleware",
"version": "0.1.0"
"version": "0.1.0",
"main": "src/index.js",
"scripts": {
"test": "ava --color --fail-fast -v"
},
"dependencies": {
"@gviagroup/jsvv": "~0.11.0"
},
"devDependencies": {
"ava": "^2.4.0",
"axios": "^0.19.0",
"express": "^4.17.1",
"nodemon": "^1.19.4"
},
"_projectTemplateVersion": "1.1.2"
}
'use strict';
const fs = require('fs');
const jsvv = require('@gviagroup/jsvv');
const rulesSchema = require('./schemas/rules');
module.exports = function ({headerName = 'X-Token', pathToRules}) {
const RULES = jsvv(JSON.parse(fs.readFileSync(pathToRules, {encoding: 'utf8'})), rulesSchema, {root: 'rules'});
const TOKEN_HEADER_LC = headerName.toLowerCase();
return async (req, res, next) => {
const token = req.headers[TOKEN_HEADER_LC] || '';
if (!hasAccess(RULES, token, req.method, req.url)) return res.status(403).send('Token is not valid');
next();
};
};
function hasAccess(rules, token, method, path) {
if (!rules.hasOwnProperty(token)) return false;
const tokenRules = rules[token];
if (typeof tokenRules === 'boolean') {
return tokenRules;
} else if (Array.isArray(tokenRules)) {
for (const tokenRule of tokenRules) {
if (isMatchesRule(tokenRule, method, path)) return true;
}
return false;
} else if (tokenRules && (typeof tokenRules === 'object')) {
return isMatchesRule(tokenRules, method, path);
}
return false;
}
function isMatchesRule(rule, method, path) {
if (rule.methods) {
if (!rule.methods.map(s => s.toLowerCase()).includes(method.toLowerCase())) return false;
}
if (rule.paths) {
for (const p of rule.paths) {
if (new RegExp(p).test(path)) return true;
}
return false;
}
return true;
}
'use strict';
const stringRequiredSchema = {
type: String,
required: true,
minLength: 1,
transforms: [String.prototype.trim]
};
const tokenSchema = {
type: String,
required: true,
transforms: [String.prototype.trim]
};
const tokenRuleSchema = {
type: Object,
required: true,
properties: {
methods: {
type: Array,
item: stringRequiredSchema,
minLength: 1
},
paths: {
type: Array,
item: stringRequiredSchema,
minLength: 1
}
},
minProperties: 1,
maxProperties: 2
};
module.exports = {
type: Object,
required: true,
propertyName: tokenSchema,
property: [
{
type: Boolean,
required: true
},
tokenRuleSchema,
{
type: Array,
required: true,
item: tokenRuleSchema,
minLength: 1
}
]
};
{
"false": false,
"true": true,
"42": true,
"foo": true,
" a b c ": true,
"GET@*": {
"methods": ["get"]
},
"GET@/test": {
"methods": ["get"],
"paths": ["^/test"]
},
"*@/test": {
"paths": ["^/test"]
},
"GET@/test/1:POST@test/2": [
{"methods": ["get"], "paths": ["^/test/1"]},
{"methods": ["post"], "paths": ["^/test/2"]}
],
"": {
"methods": ["get"]
},
" ": {
"methods": ["post"],
"_comment": "Overrides \"\""
}
}
'use strict';
const express = require('express');
const app = express()
.use(require('../src')({pathToRules: '/app/test/_rules.json'}))
.use((req, res) => res.send('Hello'))
.use((req, res) => {
console.warn(`${req.url} was not handled`);
res.status(404).set('Content-Type', 'text/plain').send(`${req.url} was not handled`);
})
.use((err, req, res, next) => {
console.error(err);
if (!res.finished) res.status(500).set('Content-Type', 'text/plain').send(err.stack);
});
let server;
module.exports = {
start() {
return new Promise(resolve => {
server = app.listen(80, () => {
console.info('Server is started.');
resolve();
});
});
},
stop() {
return new Promise((resolve, reject) => {
server.close(err => {
if (err) {
console.error('Can\'t stop the server');
return reject(err);
}
console.info('Server is stopped');
resolve();
});
});
}
};
'use strict';
const test = require('ava');
const axios = require('axios');
const server = require('./_server');
test.before(async () => {
await server.start();
});
test('"false"', async t => {
const token = 'false';
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', false);
await testAccess(t, token, 'GET', '/test', false);
await testAccess(t, token, 'POST', '/test', false);
await testAccess(t, token, 'GET', '/test/1', false);
await testAccess(t, token, 'POST', '/test/1', false);
await testAccess(t, token, 'GET', '/test/2', false);
await testAccess(t, token, 'POST', '/test/2', false);
t.pass();
});
test('"true"', async t => {
const token = 'true';
await testAccess(t, token, 'GET', '/', true);
await testAccess(t, token, 'POST', '/', true);
await testAccess(t, token, 'GET', '/test', true);
await testAccess(t, token, 'POST', '/test', true);
await testAccess(t, token, 'GET', '/test/1', true);
await testAccess(t, token, 'POST', '/test/1', true);
await testAccess(t, token, 'GET', '/test/2', true);
await testAccess(t, token, 'POST', '/test/2', true);
t.pass();
});
test('"42"', async t => {
const token = 42;
await testAccess(t, token, 'GET', '/', true);
await testAccess(t, token, 'POST', '/', true);
await testAccess(t, token, 'GET', '/test', true);
await testAccess(t, token, 'POST', '/test', true);
await testAccess(t, token, 'GET', '/test/1', true);
await testAccess(t, token, 'POST', '/test/1', true);
await testAccess(t, token, 'GET', '/test/2', true);
await testAccess(t, token, 'POST', '/test/2', true);
t.pass();
});
test('"foo"', async t => {
const token = 'foo';
await testAccess(t, token, 'GET', '/', true);
await testAccess(t, token, 'POST', '/', true);
await testAccess(t, token, 'GET', '/test', true);
await testAccess(t, token, 'POST', '/test', true);
await testAccess(t, token, 'GET', '/test/1', true);
await testAccess(t, token, 'POST', '/test/1', true);
await testAccess(t, token, 'GET', '/test/2', true);
await testAccess(t, token, 'POST', '/test/2', true);
t.pass();
});
test('" a b c "', async t => {
const token = ' a b c ';
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', false);
await testAccess(t, token, 'GET', '/test', false);
await testAccess(t, token, 'POST', '/test', false);
await testAccess(t, token, 'GET', '/test/1', false);
await testAccess(t, token, 'POST', '/test/1', false);
await testAccess(t, token, 'GET', '/test/2', false);
await testAccess(t, token, 'POST', '/test/2', false);
t.pass();
});
test('"a b c"', async t => {
const token = 'a b c';
await testAccess(t, token, 'GET', '/', true);
await testAccess(t, token, 'POST', '/', true);
await testAccess(t, token, 'GET', '/test', true);
await testAccess(t, token, 'POST', '/test', true);
await testAccess(t, token, 'GET', '/test/1', true);
await testAccess(t, token, 'POST', '/test/1', true);
await testAccess(t, token, 'GET', '/test/2', true);
await testAccess(t, token, 'POST', '/test/2', true);
t.pass();
});
test('"GET@*"', async t => {
const token = 'GET@*';
await testAccess(t, token, 'GET', '/', true);
await testAccess(t, token, 'POST', '/', false);
await testAccess(t, token, 'GET', '/test', true);
await testAccess(t, token, 'POST', '/test', false);
await testAccess(t, token, 'GET', '/test/1', true);
await testAccess(t, token, 'POST', '/test/1', false);
await testAccess(t, token, 'GET', '/test/2', true);
await testAccess(t, token, 'POST', '/test/2', false);
t.pass();
});
test('"GET@/test"', async t => {
const token = 'GET@/test';
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', false);
await testAccess(t, token, 'GET', '/test', true);
await testAccess(t, token, 'POST', '/test', false);
await testAccess(t, token, 'GET', '/test/1', true);
await testAccess(t, token, 'POST', '/test/1', false);
await testAccess(t, token, 'GET', '/test/2', true);
await testAccess(t, token, 'POST', '/test/2', false);
t.pass();
});
test('"*@/test"', async t => {
const token = '*@/test';
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', false);
await testAccess(t, token, 'GET', '/test', true);
await testAccess(t, token, 'POST', '/test', true);
await testAccess(t, token, 'GET', '/test/1', true);
await testAccess(t, token, 'POST', '/test/1', true);
await testAccess(t, token, 'GET', '/test/2', true);
await testAccess(t, token, 'POST', '/test/2', true);
t.pass();
});
test('"GET@/test/1:POST@test/2"', async t => {
const token = 'GET@/test/1:POST@test/2';
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', false);
await testAccess(t, token, 'GET', '/test', false);
await testAccess(t, token, 'POST', '/test', false);
await testAccess(t, token, 'GET', '/test/1', true);
await testAccess(t, token, 'POST', '/test/1', false);
await testAccess(t, token, 'GET', '/test/2', false);
await testAccess(t, token, 'POST', '/test/2', true);
t.pass();
});
test('"toString"', async t => {
const token = 'toString';
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', false);
await testAccess(t, token, 'GET', '/test', false);
await testAccess(t, token, 'POST', '/test', false);
await testAccess(t, token, 'GET', '/test1', false);
await testAccess(t, token, 'POST', '/test1', false);
await testAccess(t, token, 'GET', '/test2', false);
await testAccess(t, token, 'POST', '/test2', false);
t.pass();
});
test('<none>', async t => {
const token = undefined;
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', true);
await testAccess(t, token, 'GET', '/test', false);
await testAccess(t, token, 'POST', '/test', true);
await testAccess(t, token, 'GET', '/test1', false);
await testAccess(t, token, 'POST', '/test1', true);
await testAccess(t, token, 'GET', '/test2', false);
await testAccess(t, token, 'POST', '/test2', true);
t.pass();
});
test('""', async t => {
const token = '';
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', true);
await testAccess(t, token, 'GET', '/test', false);
await testAccess(t, token, 'POST', '/test', true);
await testAccess(t, token, 'GET', '/test1', false);
await testAccess(t, token, 'POST', '/test1', true);
await testAccess(t, token, 'GET', '/test2', false);
await testAccess(t, token, 'POST', '/test2', true);
t.pass();
});
test('" "', async t => {
const token = ' ';
await testAccess(t, token, 'GET', '/', false);
await testAccess(t, token, 'POST', '/', true);
await testAccess(t, token, 'GET', '/test', false);
await testAccess(t, token, 'POST', '/test', true);
await testAccess(t, token, 'GET', '/test1', false);
await testAccess(t, token, 'POST', '/test1', true);
await testAccess(t, token, 'GET', '/test2', false);
await testAccess(t, token, 'POST', '/test2', true);
t.pass();
});
test.after.always(async () => {
await server.stop();
});
async function testAccess(t, token, method, path, shouldHasAccess) {
try {
const {data} = await axios({
method,
url: `http://api${path}`,
headers: {
...((token !== undefined) ? {'X-Token': token} : {})
}
});
if (!shouldHasAccess) t.fail(`Should NOT has access to ${path} with ${method}`);
t.is(data, 'Hello');
} catch (e) {
if (e.response.status === 403) {
if (shouldHasAccess) t.fail(`Should has access to ${path} with ${method}`);
} else throw e;
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment