Add missing authorization on views endpoints

Updated ApiKeys authorization to match elsewhere
This commit is contained in:
Tom Moor 2018-02-18 10:56:56 -08:00
parent e84fb5e6ba
commit 83f32be6f7
19 changed files with 129 additions and 35 deletions

View File

@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#views.list should require authentication 1`] = `
Object {
"error": "authentication_required",
"message": "Authentication required",
"ok": false,
"status": 401,
}
`;

View File

@ -6,7 +6,9 @@ import auth from './middlewares/authentication';
import pagination from './middlewares/pagination'; import pagination from './middlewares/pagination';
import { presentApiKey } from '../presenters'; import { presentApiKey } from '../presenters';
import { ApiKey } from '../models'; import { ApiKey } from '../models';
import policy from '../policies';
const { authorize } = policy;
const router = new Router(); const router = new Router();
router.post('apiKeys.create', auth(), async ctx => { router.post('apiKeys.create', auth(), async ctx => {
@ -14,6 +16,7 @@ router.post('apiKeys.create', auth(), async ctx => {
ctx.assertPresent(name, 'name is required'); ctx.assertPresent(name, 'name is required');
const user = ctx.state.user; const user = ctx.state.user;
authorize(user, 'create', ApiKey);
const key = await ApiKey.create({ const key = await ApiKey.create({
name, name,
@ -36,9 +39,7 @@ router.post('apiKeys.list', auth(), pagination(), async ctx => {
limit: ctx.state.pagination.limit, limit: ctx.state.pagination.limit,
}); });
const data = keys.map(key => { const data = keys.map(key => presentApiKey(ctx, key));
return presentApiKey(ctx, key);
});
ctx.body = { ctx.body = {
pagination: ctx.state.pagination, pagination: ctx.state.pagination,
@ -52,10 +53,8 @@ router.post('apiKeys.delete', auth(), async ctx => {
const user = ctx.state.user; const user = ctx.state.user;
const key = await ApiKey.findById(id); const key = await ApiKey.findById(id);
authorize(user, 'delete', ApiKey);
if (!key || key.userId !== user.id) throw httpErrors.BadRequest();
// Delete the actual document
try { try {
await key.destroy(); await key.destroy();
} catch (e) { } catch (e) {

View File

@ -3,7 +3,7 @@ import TestServer from 'fetch-test-server';
import app from '..'; import app from '..';
import { flushdb, seed } from '../test/support'; import { flushdb, seed } from '../test/support';
import { buildUser } from '../test/factories'; import { buildUser } from '../test/factories';
import Collection from '../models/Collection'; import { Collection } from '../models';
const server = new TestServer(app.callback()); const server = new TestServer(app.callback());
beforeEach(flushdb); beforeEach(flushdb);

View File

@ -1,10 +1,9 @@
/* eslint-disable flowtype/require-valid-file-annotation */ /* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server'; import TestServer from 'fetch-test-server';
import app from '..'; import app from '..';
import { View, Star } from '../models'; import { Document, View, Star } from '../models';
import { flushdb, seed } from '../test/support'; import { flushdb, seed } from '../test/support';
import { buildUser } from '../test/factories'; import { buildUser } from '../test/factories';
import Document from '../models/Document';
const server = new TestServer(app.callback()); const server = new TestServer(app.callback());

View File

@ -1,7 +1,7 @@
/* eslint-disable flowtype/require-valid-file-annotation */ /* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server'; import TestServer from 'fetch-test-server';
import app from '..'; import app from '..';
import Authentication from '../models/Authentication'; import { Authentication } from '../models';
import { flushdb, seed } from '../test/support'; import { flushdb, seed } from '../test/support';
import * as Slack from '../slack'; import * as Slack from '../slack';

View File

@ -1,6 +1,6 @@
/* eslint-disable flowtype/require-valid-file-annotation */ /* eslint-disable flowtype/require-valid-file-annotation */
import { flushdb, seed } from '../../test/support'; import { flushdb, seed } from '../../test/support';
import ApiKey from '../../models/ApiKey'; import { ApiKey } from '../../models';
import randomstring from 'randomstring'; import randomstring from 'randomstring';
import auth from './authentication'; import auth from './authentication';

View File

@ -2,8 +2,7 @@
import Router from 'koa-router'; import Router from 'koa-router';
import httpErrors from 'http-errors'; import httpErrors from 'http-errors';
import Team from '../models/Team'; import { Team, User } from '../models';
import User from '../models/User';
import auth from './middlewares/authentication'; import auth from './middlewares/authentication';
import pagination from './middlewares/pagination'; import pagination from './middlewares/pagination';

View File

@ -2,7 +2,7 @@
import uuid from 'uuid'; import uuid from 'uuid';
import Router from 'koa-router'; import Router from 'koa-router';
import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3'; import { makePolicy, signPolicy, publicS3Endpoint } from '../utils/s3';
import Event from '../models/Event'; import { Event } from '../models';
import auth from './middlewares/authentication'; import auth from './middlewares/authentication';
import { presentUser } from '../presenters'; import { presentUser } from '../presenters';

View File

@ -1,24 +1,26 @@
// @flow // @flow
import Router from 'koa-router'; import Router from 'koa-router';
import httpErrors from 'http-errors';
import auth from './middlewares/authentication'; import auth from './middlewares/authentication';
import { presentView } from '../presenters'; import { presentView } from '../presenters';
import { View, Document } from '../models'; import { View, Document } from '../models';
import policy from '../policies';
const { authorize } = policy;
const router = new Router(); const router = new Router();
router.post('views.list', auth(), async ctx => { router.post('views.list', auth(), async ctx => {
const { id } = ctx.body; const { id } = ctx.body;
ctx.assertPresent(id, 'id is required'); ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const document = await Document.findById(id);
authorize(user, 'read', document);
const views = await View.findAll({ const views = await View.findAll({
where: { where: { documentId: id },
documentId: id,
},
order: [['updatedAt', 'DESC']], order: [['updatedAt', 'DESC']],
}); });
// Collectiones
let users = []; let users = [];
let count = 0; let count = 0;
await Promise.all( await Promise.all(
@ -42,11 +44,13 @@ router.post('views.create', auth(), async ctx => {
const user = ctx.state.user; const user = ctx.state.user;
const document = await Document.findById(id); const document = await Document.findById(id);
authorize(user, 'read', document);
if (!document || document.teamId !== user.teamId)
throw httpErrors.BadRequest();
await View.increment({ documentId: document.id, userId: user.id }); await View.increment({ documentId: document.id, userId: user.id });
ctx.body = {
success: true,
};
}); });
export default router; export default router;

73
server/api/views.test.js Normal file
View File

@ -0,0 +1,73 @@
/* eslint-disable flowtype/require-valid-file-annotation */
import TestServer from 'fetch-test-server';
import app from '..';
import { flushdb, seed } from '../test/support';
import { buildUser } from '../test/factories';
const server = new TestServer(app.callback());
beforeEach(flushdb);
afterAll(server.close);
describe('#views.list', async () => {
it('should return views for a document', async () => {
const { user, document } = await seed();
const res = await server.post('/api/views.list', {
body: { token: user.getJwtToken(), id: document.id },
});
expect(res.status).toEqual(200);
});
it('should require authentication', async () => {
const { document } = await seed();
const res = await server.post('/api/views.list', {
body: { id: document.id },
});
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should require authorization', async () => {
const { document } = await seed();
const user = await buildUser();
const res = await server.post('/api/views.list', {
body: { token: user.getJwtToken(), id: document.id },
});
expect(res.status).toEqual(403);
});
});
describe('#views.create', async () => {
it('should allow creating a view record for document', async () => {
const { user, document } = await seed();
const res = await server.post('/api/views.create', {
body: { token: user.getJwtToken(), id: document.id },
});
const body = await res.json();
expect(res.status).toEqual(200);
expect(body.success).toBe(true);
});
it('should require authentication', async () => {
const { document } = await seed();
const res = await server.post('/api/views.create', {
body: { id: document.id },
});
const body = await res.json();
expect(res.status).toEqual(401);
expect(body).toMatchSnapshot();
});
it('should require authorization', async () => {
const { document } = await seed();
const user = await buildUser();
const res = await server.post('/api/views.create', {
body: { token: user.getJwtToken(), id: document.id },
});
expect(res.status).toEqual(403);
});
});

View File

@ -2,8 +2,7 @@
import Queue from 'bull'; import Queue from 'bull';
import debug from 'debug'; import debug from 'debug';
import services from '../services'; import services from '../services';
import Document from './models/Document'; import { Collection, Document } from './models';
import Collection from './models/Collection';
type DocumentEvent = { type DocumentEvent = {
name: 'documents.create', name: 'documents.create',

View File

@ -1,7 +1,6 @@
/* eslint-disable flowtype/require-valid-file-annotation */ /* eslint-disable flowtype/require-valid-file-annotation */
import { flushdb, seed } from '../test/support'; import { flushdb, seed } from '../test/support';
import Collection from '../models/Collection'; import { Collection, Document } from '../models';
import Document from '../models/Document';
beforeEach(flushdb); beforeEach(flushdb);
beforeEach(jest.resetAllMocks); beforeEach(jest.resetAllMocks);

14
server/policies/apiKey.js Normal file
View File

@ -0,0 +1,14 @@
// @flow
import policy from './policy';
import { ApiKey, User } from '../models';
const { allow } = policy;
allow(User, 'create', ApiKey);
allow(
User,
['read', 'update', 'delete'],
ApiKey,
(user, apiKey) => user && user.id === apiKey.userId
);

View File

@ -1,7 +1,6 @@
// @flow // @flow
import policy from './policy'; import policy from './policy';
import Collection from '../models/Collection'; import { Collection, User } from '../models';
import User from '../models/User';
const { allow } = policy; const { allow } = policy;

View File

@ -1,7 +1,6 @@
// @flow // @flow
import policy from './policy'; import policy from './policy';
import Document from '../models/Document'; import { Document, User } from '../models';
import User from '../models/User';
const { allow } = policy; const { allow } = policy;

View File

@ -1,7 +1,8 @@
// @flow // @flow
import policy from './policy'; import policy from './policy';
import './document'; import './apiKey';
import './collection'; import './collection';
import './document';
import './user'; import './user';
export default policy; export default policy;

View File

@ -1,6 +1,6 @@
// @flow // @flow
import policy from './policy'; import policy from './policy';
import User from '../models/User'; import { User } from '../models';
const { allow } = policy; const { allow } = policy;

View File

@ -1,5 +1,5 @@
// @flow // @flow
import User from '../models/User'; import { User } from '../models';
type Options = { type Options = {
includeDetails?: boolean, includeDetails?: boolean,

View File

@ -1,6 +1,5 @@
// @flow // @flow
import Team from '../models/Team'; import { Team, User } from '../models';
import User from '../models/User';
import uuid from 'uuid'; import uuid from 'uuid';
let count = 0; let count = 0;