Add missing authorization on views endpoints
Updated ApiKeys authorization to match elsewhere
This commit is contained in:
parent
e84fb5e6ba
commit
83f32be6f7
10
server/api/__snapshots__/views.test.js.snap
Normal file
10
server/api/__snapshots__/views.test.js.snap
Normal 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,
|
||||||
|
}
|
||||||
|
`;
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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
73
server/api/views.test.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
@ -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',
|
||||||
|
@ -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
14
server/policies/apiKey.js
Normal 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
|
||||||
|
);
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import User from '../models/User';
|
import { User } from '../models';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
includeDetails?: boolean,
|
includeDetails?: boolean,
|
||||||
|
@ -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;
|
||||||
|
Reference in New Issue
Block a user