Commit f0370302 authored by Vadym Gidulian's avatar Vadym Gidulian

Implemented logic

parent 1a2bc699
/.data
.env
# Dependency directories
node_modules/
# Logs
logs/
*.log
npm-debug.log*
# Optional npm cache directory
.npm/
# Optional REPL history
.node_repl_history/
*
!/src/**
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", "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