From c6ce54ee877fc22f658a37a066f58d11136228a1 Mon Sep 17 00:00:00 2001 From: "R. Tyler Croy" Date: Thu, 20 Sep 2018 17:06:42 -0700 Subject: [PATCH] Add support for restricting access via grants Fixes #4 --- Makefile | 2 +- seeders/20180920225942-specific-types.js | 15 +++++++++++ src/controllers/dashboard.ts | 2 +- src/controllers/export.ts | 5 ++-- src/hooks/apply-grant.ts | 24 +++++++++++++++++ src/hooks/authorize.ts | 13 ++++----- src/services/bulk.ts | 17 ++++++------ src/services/events.ts | 5 +++- test/bulk.test.ts | 34 ------------------------ 9 files changed, 62 insertions(+), 55 deletions(-) create mode 100644 seeders/20180920225942-specific-types.js create mode 100644 src/hooks/apply-grant.ts delete mode 100644 test/bulk.test.ts diff --git a/Makefile b/Makefile index ee4dc35..3dc9d58 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,7 @@ migrate: depends @echo ">> waiting a moment to make sure the database comes online.." @sleep 3 $(COMPOSE) run --rm node \ - /usr/local/bin/node $(SEQUELIZE) db:migrate + /usr/local/bin/node $(SEQUELIZE) db:migrate && \ $(COMPOSE) run --rm node \ /usr/local/bin/node $(SEQUELIZE) db:seed:all diff --git a/seeders/20180920225942-specific-types.js b/seeders/20180920225942-specific-types.js new file mode 100644 index 0000000..9e10199 --- /dev/null +++ b/seeders/20180920225942-specific-types.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: (queryInterface, Sequelize) => { + return queryInterface.bulkInsert('grants', [ + { + name: 'rtyler', + type: 'jest-example', + }, + ]); + }, + + down: (queryInterface, Sequelize) => { + } +}; diff --git a/src/controllers/dashboard.ts b/src/controllers/dashboard.ts index b40fd35..5a94aa2 100644 --- a/src/controllers/dashboard.ts +++ b/src/controllers/dashboard.ts @@ -29,6 +29,6 @@ export default (app) => { user: (req as any).user, query: req.query, })) - .catch(err => res.render('notauthorized')); + .catch(next); }); }; diff --git a/src/controllers/export.ts b/src/controllers/export.ts index 08ec81a..39bb6a2 100644 --- a/src/controllers/export.ts +++ b/src/controllers/export.ts @@ -23,6 +23,7 @@ export default (app) => { res.setHeader('Content-Type', 'application/json'); res.send(result); res.end(); - }); - }); + }) + .catch(next); + }); }; diff --git a/src/hooks/apply-grant.ts b/src/hooks/apply-grant.ts new file mode 100644 index 0000000..0a63dbf --- /dev/null +++ b/src/hooks/apply-grant.ts @@ -0,0 +1,24 @@ +import logger from '../logger'; + +/** + * This hook will apply the grants for the current user to the search query + */ +export default () => { + return async context => { + const grantedTypes = context.data.grants.filter(g => g != '*'); + + if (grantedTypes) { + /* + * If there are wildcard grants, we don't need to apply any restrictions + * to the query + */ + if (context.data.grants.length > grantedTypes.length) { + return context; + } + Object.assign(context.params.query, { + 'type': grantedTypes, + }); + } + return context; + } +}; diff --git a/src/hooks/authorize.ts b/src/hooks/authorize.ts index 6d5db02..2c0e001 100644 --- a/src/hooks/authorize.ts +++ b/src/hooks/authorize.ts @@ -17,20 +17,17 @@ export default () => { return context.app.service('grants').find({ query: { name: name, - $or : [ - { - type: type, - }, - { - type: '*', - }, - ], }, }) .then((records) => { if (records.length === 0) { throw new Forbidden('Not allowed, sorry buddy'); } + + if (!context.data) { + context.data = {}; + } + context.data.grants = records.map(r => r.type); return context; }); }; diff --git a/src/services/bulk.ts b/src/services/bulk.ts index fc9ffba..ebcfa1d 100644 --- a/src/services/bulk.ts +++ b/src/services/bulk.ts @@ -4,6 +4,7 @@ */ import { Application, HooksObject, Params, SKIP } from '@feathersjs/feathers'; +import { BadRequest, NotFound } from '@feathersjs/errors'; import { QueryTypes } from 'sequelize'; import authorize from '../hooks/authorize'; @@ -14,16 +15,11 @@ import Event from '../models/event'; export const bulkHooks : HooksObject = { before: { all: [ + authorize(), (context) => { - /* - * Allow skipping for our tests - */ - if (process.env.NODE_ENV != 'production') { - return SKIP; - } + context.params.grants = context.data.grants return context; }, - authorize(), ], }, after: {}, @@ -42,8 +38,13 @@ export class Bulk { public async find(params : Params) : Promise { if (!params.query.type) { - return Promise.resolve([]); + return Promise.reject(new BadRequest('Request must have a `type` in the URL')); } + const grantedTypes = params.grants.filter(g => (g == '*') || (g == params.query.type)); + if (grantedTypes.length == 0) { + return Promise.reject(new NotFound('No data found')); + } + /* * This is clearly stupid. I have no idea how we'll query very large * datasets from PostgreSQL but this at least gets us _everything_ diff --git a/src/services/events.ts b/src/services/events.ts index c2e2ed5..330a5ab 100644 --- a/src/services/events.ts +++ b/src/services/events.ts @@ -5,9 +5,10 @@ import { Application, HooksObject } from '@feathersjs/feathers'; import service from 'feathers-sequelize'; -import { DataTypes } from 'sequelize'; +import { Operators, DataTypes } from 'sequelize'; import authorize from '../hooks/authorize'; +import applyGrant from '../hooks/apply-grant'; import logger from '../logger'; import db from '../models'; import Event from '../models/event'; @@ -18,9 +19,11 @@ export const eventsHooks : HooksObject = { ], find: [ authorize(), + applyGrant(), ], get: [ authorize(), + applyGrant(), ], create: [ /* diff --git a/test/bulk.test.ts b/test/bulk.test.ts deleted file mode 100644 index 6efba71..0000000 --- a/test/bulk.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import url from 'url'; -import request from 'request-promise'; - -import app from '../src/app'; -import { Bulk } from '../src/service/bulk'; - -// Offsetting a bit to ensure that we can watch and run at the same time -const port = (app.get('port') || 3030) + 10; -const getUrl = pathname => url.format({ - hostname: app.get('host') || 'localhost', - protocol: 'http', - port, - pathname -}); - -describe('Acceptance tests for /evetns/bulk', () => { - beforeEach((done) => { - this.server = app.listen(port); - this.server.once('listening', () => done()); - }); - afterEach(done => this.server.close(done)); - - describe('GET on /events/bulk', () => { - it('without a type parameter should be empty', () => { - return request(getUrl('/events/bulk'), { - json: true, - resolveWithFullResponse: true, - }).then((response) => { - expect(response.statusCode).toEqual(200); - expect(response.body).toHaveLength(0); - }); - }); - }); -});