diff --git a/app/manage/index.js b/app/manage/index.js index 4ad15363..a2416dec 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -40,9 +40,9 @@ function manageRouter (nextApp) { /** * List / Create / Delete clients */ - router.get('/api/clients', can('clients:view'), getClients) - router.post('/api/clients', can('clients:create'), createClient) - router.delete('/api/clients/:id', can('clients:delete'), deleteClient) + router.get('/api/clients', can('clients'), getClients) + router.post('/api/clients', can('clients'), createClient) + router.delete('/api/clients/:id', can('client:delete'), deleteClient) /** * List / Create / Delete teams @@ -59,11 +59,11 @@ function manageRouter (nextApp) { /** * Page renders */ - router.get('/clients', can('clients:view'), (req, res) => { + router.get('/clients', can('clients'), (req, res) => { return nextApp.render(req, res, '/clients') }) - router.get('/profile', can('clients:view'), (req, res) => { + router.get('/profile', can('clients'), (req, res) => { return nextApp.render(req, res, '/profile') }) diff --git a/app/manage/permissions/view-clients.js b/app/manage/permissions/clients.js similarity index 82% rename from app/manage/permissions/view-clients.js rename to app/manage/permissions/clients.js index 16f9b9fd..5444fe4a 100644 --- a/app/manage/permissions/view-clients.js +++ b/app/manage/permissions/clients.js @@ -1,7 +1,7 @@ const db = require('../../db') /** - * clients:view + * clients * * To access the clients API, requests need to be authenticated * with a signed up user @@ -10,7 +10,7 @@ const db = require('../../db') * @param {Object} params request parameters * @returns {boolean} can the request go through? */ -async function viewClients (uid) { +async function clients (uid) { let conn = await db() const [user] = await conn('users').where('id', uid) if (user) { @@ -18,4 +18,4 @@ async function viewClients (uid) { } } -module.exports = viewClients +module.exports = clients diff --git a/app/manage/permissions/delete-client.js b/app/manage/permissions/delete-client.js new file mode 100644 index 00000000..2a7c7651 --- /dev/null +++ b/app/manage/permissions/delete-client.js @@ -0,0 +1,18 @@ +const db = require('../../db') + +/** + * client:delete + * + * To delete a client, an authenticated user must own this client + * + * + * @param uid + * @returns {undefined} + */ +async function deleteClient (uid, { id }) { + let conn = await db() + const [client] = await conn('hydra_client').where('id', id) + return (client.owner === uid) +} + +module.exports = deleteClient diff --git a/app/manage/permissions/index.js b/app/manage/permissions/index.js index 83857c0f..5bfc0319 100644 --- a/app/manage/permissions/index.js +++ b/app/manage/permissions/index.js @@ -14,7 +14,8 @@ const teamPermissions = { } const clientPermissions = { - 'clients:view': require('./view-clients') + 'clients': require('./clients'), + 'client:delete': require('./delete-client') } const permissions = mergeAll([ diff --git a/app/tests/permissions/clients.js b/app/tests/permissions/clients.js new file mode 100644 index 00000000..84dc62fe --- /dev/null +++ b/app/tests/permissions/clients.js @@ -0,0 +1,49 @@ +const test = require('ava') +const db = require('../../db') +const path = require('path') +const hydra = require('../../lib/hydra') +const sinon = require('sinon') + +const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations') + +let agent +test.before(async () => { + const conn = await db() + await conn.migrate.latest({ directory: migrationsDirectory }) + + // seed + await conn('users').insert({ id: 100 }) + + // stub hydra introspect + let introspectStub = sinon.stub(hydra, 'introspect') + introspectStub.withArgs('validToken').returns({ + active: true, + sub: '100' + }) + introspectStub.withArgs('invalidToken').returns({ active: false }) + + // stub hydra get clients + let getClientsStub = sinon.stub('hydra', 'getClients') + getClientsStub.returns([]) + + agent = require('supertest').agent(await require('../../index')()) +}) + +test.after.always(async () => { + const conn = await db() + await conn.migrate.rollback({ directory: migrationsDirectory }) + conn.destroy() +}) + +test('an authenticated user can view their clients', async t => { + let res = await agent.get('/api/clients') + .set('Authorization', `Bearer validToken`) + + t.is(res.status, 200) +}) + +test('an unauthenticated user cannot view their clients', async t => { + let res = await agent.get('/api/clients') + + t.is(res.status, 401) +}) diff --git a/app/tests/permissions/delete-client.test.js b/app/tests/permissions/delete-client.test.js new file mode 100644 index 00000000..659e98b0 --- /dev/null +++ b/app/tests/permissions/delete-client.test.js @@ -0,0 +1,61 @@ +const test = require('ava') +const db = require('../../db') +const path = require('path') +const hydra = require('../../lib/hydra') +const sinon = require('sinon') + +const migrationsDirectory = path.join(__dirname, '..', '..', 'db', 'migrations') + +let agent +test.before(async () => { + const conn = await db() + await conn.migrate.latest({ directory: migrationsDirectory }) + await conn.schema.createTable('hydra_client', t => { + // schema at https://github.com/ory/hydra/blob/master/client/manager_sql.go + t.string('id') + t.string('owner') + }) + + // seed + await conn('hydra_client').insert({ id: 999, owner: '100' }) + await conn('hydra_client').insert({ id: 998, owner: '101' }) + + // stub hydra introspect + let introspectStub = sinon.stub(hydra, 'introspect') + introspectStub.withArgs('validToken').returns({ + active: true, + sub: '100' + }) + introspectStub.withArgs('differentUser').returns({ + active: true, + sub: '101' + }) + introspectStub.withArgs('invalidToken').returns({ active: false }) + + // stub hydra delete client + let deleteClientStub = sinon.stub(hydra, 'deleteClient') + deleteClientStub.returns(Promise.resolve(true)) + + agent = require('supertest').agent(await require('../../index')()) +}) + +test.after.always(async () => { + const conn = await db() + await conn.schema.dropTable('hydra_client') + await conn.migrate.rollback({ directory: migrationsDirectory }) + conn.destroy() +}) + +test('a user can delete a client they created', async t => { + let res = await agent.delete('/api/clients/999') + .set('Authorization', 'Bearer validToken') + + t.is(res.status, 200) +}) + +test("a user can't delete a client they don't own", async t => { + let res = await agent.delete('/api/clients/998') + .set('Authorization', 'Bearer validToken') + + t.is(res.status, 401) +}) diff --git a/components/clients.js b/components/clients.js index c9e42120..877ee8b0 100644 --- a/components/clients.js +++ b/components/clients.js @@ -131,7 +131,7 @@ class Clients extends Component {
Add an OAuth app to integrate with OSM/Hydra.
+Add an OAuth app to integrate with the OSM Teams API
⚠️ Save this information, we won't show it again.
+ {newClient(this.state.newClient)} +⚠️ Save this information, we won't show it again.
- {newClient(this.state.newClient)} -