Add support for restricting access via grants

Fixes #4
This commit is contained in:
R. Tyler Croy 2018-09-20 17:06:42 -07:00
parent 4900aafca9
commit c6ce54ee87
No known key found for this signature in database
GPG Key ID: 1426C7DC3F51E16F
9 changed files with 62 additions and 55 deletions

View File

@ -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

View File

@ -0,0 +1,15 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('grants', [
{
name: 'rtyler',
type: 'jest-example',
},
]);
},
down: (queryInterface, Sequelize) => {
}
};

View File

@ -29,6 +29,6 @@ export default (app) => {
user: (req as any).user,
query: req.query,
}))
.catch(err => res.render('notauthorized'));
.catch(next);
});
};

View File

@ -23,6 +23,7 @@ export default (app) => {
res.setHeader('Content-Type', 'application/json');
res.send(result);
res.end();
});
});
})
.catch(next);
});
};

24
src/hooks/apply-grant.ts Normal file
View File

@ -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;
}
};

View File

@ -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;
});
};

View File

@ -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<any> {
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_

View File

@ -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: [
/*

View File

@ -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);
});
});
});
});