feat(streams api): scaffolded routes and tests for basic stream routes

This commit is contained in:
Dimitrie Stefanescu
2020-04-05 20:48:45 +01:00
parent 414edeee11
commit 94b5976bd2
8 changed files with 228 additions and 70 deletions
+9 -7
View File
@@ -3,6 +3,7 @@
const express = require( 'express' )
const logger = require( 'morgan-debug' )
const bodyParser = require( 'body-parser' )
const debug = require( 'debug' )( 'speckle:errors' )
exports.init = ( ) => {
const app = express( )
@@ -15,13 +16,14 @@ exports.init = ( ) => {
app.use( bodyParser.urlencoded( { extended: false } ) )
// Error responses
// app.use( ( err, req, res, next ) => {
// res.status( err.status || 500 )
// res.json( {
// message: err.message,
// error: err
// } )
// } )
app.use( ( err, req, res, next ) => {
debug( err )
res.status( err.status || 500 )
res.json( {
message: err.message,
error: err
} )
} )
app.get( '/', ( req, res ) => {
res.send( { fantastic: 'speckle' } )
+4 -4
View File
@@ -10,7 +10,7 @@ exports.up = async knex => {
table.uuid( 'id' ).defaultTo( knex.raw( 'gen_random_uuid()' ) ).unique( ).primary( )
table.text( 'name' )
table.text( 'description' )
table.boolean( 'public' ).defaultTo( true )
table.boolean( 'isPublic' ).defaultTo( true )
table.uuid( 'cloned_from' ).references( 'id' ).inTable( 'streams' )
table.timestamp( 'created_at' ).defaultTo( knex.fn.now( ) )
table.timestamp( 'updated_at' ).defaultTo( knex.fn.now( ) )
@@ -29,9 +29,9 @@ exports.up = async knex => {
await knex.schema.createTable( 'stream_acl', table => {
table.uuid( 'user_id' ).references( 'id' ).inTable( 'users' ).notNullable( )
table.uuid( 'stream_id' ).references( 'id' ).inTable( 'streams' ).notNullable( )
table.primary( [ 'user_id', 'stream_id' ] )
table.unique( [ 'user_id', 'stream_id' ] )
table.uuid( 'resource_id' ).references( 'id' ).inTable( 'streams' ).notNullable( )
table.primary( [ 'user_id', 'resource_id' ] )
table.unique( [ 'user_id', 'resource_id' ] )
table.specificType( 'role', 'speckle_acl_role_type' ).defaultTo( 'write' )
} )
+47 -8
View File
@@ -1,6 +1,6 @@
'use strict'
const { getStream, createStream, updateStream } = require( './services' )
const debug = require( 'debug' )( 'speckle:test' )
const { getStream, createStream, updateStream, grantPermissionsStream, revokePermissionsStream } = require( './services' )
module.exports = {
@@ -10,24 +10,63 @@ module.exports = {
},
getStream: async ( req, res, next ) => {
res.status( 418 ).send( "meeps" )
next( )
try {
let stream = await getStream( req.params.resourceId )
res.status( 200 ).send( stream )
next( )
} catch ( err ) {
next( err )
}
},
createStream: async ( req, res, next ) => {
try {
let id = await createStream( req.body, req.user.userId )
let id = await createStream( req.body, req.user.id )
res.status( 201 ).send( { success: true, id: id } )
req.eventData = { id: id, userId: req.user.userId }
next( )
} catch ( err ) {
console.log( err )
next( err )
}
},
updateStream: async ( req, res, next ) => {
res.send( { todo: true } )
next( )
try {
let id = await updateStream( req.params.resourceId, req.body )
res.status( 200 ).send( { success: true, id: id } )
req.eventData = { id: id, userId: req.user.userId }
next( )
} catch ( err ) {
next( err )
}
},
grantPermissions: async ( req, res, next ) => {
try {
await grantPermissionsStream( req.params.resourceId, req.body.id, req.body.role )
next( )
} catch ( err ) {
next( err )
}
},
revokePermissions: async ( req, res, next ) => {
try {
} catch ( err ) {
next( err )
}
},
getStreamUsers: async ( req, res, next ) => {
try {
} catch ( err ) {
next( err )
}
}
}
+30 -8
View File
@@ -1,6 +1,6 @@
'use strict'
const root = require( 'app-root-path' )
const { getStreams, getStream, createStream, updateStream } = require( './controllers' )
const { getStreams, getStream, createStream, updateStream, grantPermissions, revokePermissions, getStreamUsers } = require( './controllers' )
const { authenticate, authorize, announce } = require( `${root}/modules/shared` )
const streams = require( 'express' ).Router( { mergeParams: true } )
@@ -13,21 +13,43 @@ streams.get(
getStreams )
streams.get(
'/streams/:streamId',
authenticate( 'streams:read' ),
authorize,
'/streams/:resourceId',
authenticate( 'streams:read', false ),
authorize( 'stream_acl', 'streams', 'read' ),
getStream )
streams.post(
'/streams',
authenticate( 'streams:write' ),
authorize,
createStream,
announce( 'stream-created', 'user' ) )
streams.put(
'/streams/:streamId',
'/streams/:resourceId',
authenticate( 'streams:write' ),
authorize,
authorize( 'stream_acl', 'streams', 'write' ),
updateStream,
announce( 'stream-updated', 'stream' ) )
announce( 'stream-updated', 'stream' ) )
streams.post(
'/streams/:resourceId/users',
authenticate( 'streams:write' ),
authorize( 'stream_acl', 'streams', 'owner' ),
grantPermissions,
announce( 'stream-created', 'user' )
)
streams.get(
'/streams/:resourceId/users',
authenticate( 'streams:read' ),
authorize( 'stream_acl', 'streams', 'read' ),
getStreamUsers
)
streams.delete(
'/streams/:resourceId/users',
authenticate( 'streams:write' ),
authorize( 'stream_acl', 'streams', 'owner' ),
revokePermissions,
announce( 'stream-deleted', 'user' )
)
+7 -7
View File
@@ -14,7 +14,7 @@ module.exports = {
stream.updated_at = knex.fn.now( )
let [ res ] = await Streams( ).returning( 'id' ).insert( stream )
await Acl( ).insert( { user_id: ownerId, stream_id: res, role: 'owner' } )
await Acl( ).insert( { user_id: ownerId, resource_id: res, role: 'owner' } )
return res
},
@@ -32,19 +32,19 @@ module.exports = {
grantPermissionsStream: async ( streamId, userId, role ) => {
if ( role === 'owner' ) {
let [ ownerAcl ] = await Acl( ).where( { stream_id: streamId, role: 'owner' } ).returning( '*' ).del( )
await Acl( ).insert( { stream_id: streamId, user_id: ownerAcl.user_id, role: 'write' } )
let [ ownerAcl ] = await Acl( ).where( { resource_id: streamId, role: 'owner' } ).returning( '*' ).del( )
await Acl( ).insert( { resource_id: streamId, user_id: ownerAcl.user_id, role: 'write' } )
}
// upsert
let query = Acl( ).insert( { user_id: userId, stream_id: streamId, role: role } ).toString( ) + ` on conflict on constraint stream_acl_pkey do update set role=excluded.role`
let query = Acl( ).insert( { user_id: userId, resource_id: streamId, role: role } ).toString( ) + ` on conflict on constraint stream_acl_pkey do update set role=excluded.role`
await knex.raw( query )
},
revokePermissionsStream: async ( streamId, userId ) => {
let streamAclEntries = Acl( ).where( { stream_id: streamId } ).select( '*' )
let delCount = await Acl( ).where( { stream_id: streamId, user_id: userId } ).whereNot( { role: 'owner' } ).del( )
let streamAclEntries = Acl( ).where( { resource_id: streamId } ).select( '*' )
let delCount = await Acl( ).where( { resource_id: streamId, user_id: userId } ).whereNot( { role: 'owner' } ).del( )
if ( delCount === 0 )
throw new Error( 'Could not revoke permissions for user. Is he an owner?' )
},
@@ -67,7 +67,7 @@ module.exports = {
limit = limit || 100
return Acl( ).where( { user_id: userId } )
.rightJoin( 'streams', { 'streams.id': 'stream_acl.stream_id' } )
.rightJoin( 'streams', { 'streams.id': 'stream_acl.resource_id' } )
.limit( limit ).offset( offset )
},
}
+61 -16
View File
@@ -112,7 +112,7 @@ describe( 'Streams', ( ) => {
}
} )
it( 'A stream should not have more than one owner', async ( ) => {
it( '🤔 DUBIOUS: A stream should not have more than one owner', async ( ) => {
let newStream = { name: 'XXX' }
newStream.id = await createStream( newStream, userOne.id )
await grantPermissionsStream( newStream.id, userTwo.id, 'owner' )
@@ -134,44 +134,89 @@ describe( 'Streams', ( ) => {
// The express app
let app
let token
let userA = { username: 'A', name: 'DimitrieA ', email: 'didimitrie+a@gmail.com', password: 'sn3aky-1337-b1m' }
let userB = { username: 'B', name: 'DimitrieB ', email: 'didimitrie+b@gmail.com', password: 'sn3aky-1337-b1m' }
let tokenA
let tokenB
before( async ( ) => {
app = init( )
token = await createToken( userOne.id, 'Generic Token', [ 'streams:read', 'streams:write' ] )
userA.id = await createUser( userA )
userB.id = await createUser( userB )
tokenA = await createToken( userA.id, 'Generic Token', [ 'streams:read', 'streams:write' ] )
tokenB = await createToken( userB.id, 'Generic Token', [ 'streams:read', 'streams:write' ] )
} )
let myTestStream = { name: 'woowowo', id: 'noids', description: 'wonderful test stream' }
let privateStream = { name: 'woowowo', id: 'noids', description: 'wonderful test stream', isPublic: false }
let publicStream = { name: 'i am public', isPublic: true }
it( 'Should create a stream', async ( ) => {
const res = await chai.request( app ).post( '/streams' ).set( 'Authorization', `Bearer ${token}` ).send( myTestStream )
const res = await chai.request( app ).post( '/streams' ).set( 'Authorization', `Bearer ${tokenA}` ).send( privateStream )
expect( res ).to.have.status( 201 )
expect( res.body ).to.have.property( 'id' )
myTestStream.id = res.body.id
privateStream.id = res.body.id
const second = await chai.request( app ).post( '/streams' ).set( 'Authorization', `Bearer ${tokenA}` ).send( publicStream )
expect( second ).to.have.status( 201 )
expect( second.body ).to.have.property( 'id' )
publicStream.id = second.body.id
} )
it( 'Should get a stream', async ( ) => {
assert.fail( 'Not implemented yet.' )
const res = await chai.request( app ).get( `/streams/${myTestStream.id}` ).set( 'Authorization', `Bearer ${token}` )
const res = await chai.request( app ).get( `/streams/${privateStream.id}` ).set( 'Authorization', `Bearer ${tokenA}` )
expect( res ).to.have.status( 200 )
expect( res.body ).to.have.property( 'id' )
expect( res.body ).to.have.property( 'name' )
} )
it( 'Should update a stream', async ( ) => {
assert.fail( 'Not implemented yet.' )
it( 'Should get a public stream, even if user is anonymous', async ( ) => {
const res = await chai.request( app ).get( `/streams/${publicStream.id}` )
expect( res ).to.have.status( 200 )
expect( res.body ).to.have.property( 'id' )
expect( res.body ).to.have.property( 'name' )
} )
const res = await chai.request( app ).put( `/streams/${myTestStream.id}` ).send( { name: 'new name' } )
const resUpdated = await chai.request( app ).get( `/streams/${myTestStream.id}` )
it( 'Should not get a private stream if user is not authenticated', async ( ) => {
const res = await chai.request( app ).get( `/streams/${privateStream.id}` )
expect( res ).to.have.status( 401 )
} )
it( 'Should not get a private stream if the user does not have access to it', async ( ) => {
const res = await chai.request( app ).get( `/streams/${privateStream.id}` ).set( 'Authorization', `Bearer ${tokenB}` )
expect( res ).to.have.status( 401 )
} )
it( 'Should update a stream', async ( ) => {
const res = await chai.request( app ).put( `/streams/${publicStream.id}` ).send( { name: 'new name' } ).set( 'Authorization', `Bearer ${tokenA}` )
const resUpdated = await chai.request( app ).get( `/streams/${publicStream.id}` )
expect( res ).to.have.status( 200 )
expect( res.body ).to.have.property( 'id' )
expect( resUpdated ).to.have.status( 200 )
expect( resUpdated ).to.have.property( 'name' )
expect( resUpdated.name ).to.equal( 'new name' )
expect( resUpdated.body ).to.have.property( 'name' )
expect( resUpdated.body.name ).to.equal( 'new name' )
} )
it( 'Should grant permissions on a stream', async ( ) => {
const shareRes = await chai.request( app ).post( `/streams/users/${privateStream.id}` ).send( { id: userB.id, role: 'read' } ).set( 'Authorization', `Bearer ${tokenA}` )
console.log(shareRes)
expect( shareRes ).to.have.status( 200 )
const userBRes = await chai.request( app ).get( `/streams/${privateStream.id}` ).set( 'Authorization', `Bearer ${tokenB}` )
console.log(userBRes.status)
expect( userBRes ).to.have.status( 200 )
expect( userBRes.body ).to.have.property( 'name' )
expect( userBRes.body ).to.have.property( 'description' )
} )
it( 'Should revoke permissions on a stream', async ( ) => {
assert.fail( )
} )
it( 'Should delete a stream', async ( ) => {
+68 -18
View File
@@ -1,46 +1,96 @@
'use strict'
const debug = require( 'debug' )( 'speckle:test' )
const debug = require( 'debug' )( 'speckle:middleware' )
const root = require( 'app-root-path' )
const knex = require( `${root}/db/knex` )
const { validateToken } = require( `${root}/modules/core/users/services` )
/*
Authentication: checks bearer token validity and scope validation
*/
// TODO: Cache results
function authenticate( scope, mandatory ) {
mandatory = mandatory || true
mandatory = mandatory !== false
return async ( req, res, next ) => {
debug( `🔑 authenticate middleware called` )
if ( !req.headers.authorization && mandatory ) {
return res.status( 403 ).send( { error: 'No credentials provided' } ) // next (err)?
}
let token = req.headers.authorization.split( ' ' )[ 1 ]
let { valid, scopes, userId } = await validateToken( token )
if ( req.headers.authorization ) {
let token = req.headers.authorization.split( ' ' )[ 1 ]
let { valid, scopes, userId } = await validateToken( token )
if ( !valid && mandatory ) {
return res.status( 403 ).send( { error: 'Invalid authorization' } )
if ( !valid && mandatory ) {
return res.status( 403 ).send( { error: 'Invalid authorization' } )
}
if ( scopes.indexOf( scope ) === -1 && scopes.indexOf( '*' ) === -1 ) {
return res.status( 403 ).send( { error: 'Invalid scope' } )
}
req.user = { id: userId, scopes: scopes }
return next( )
}
if ( scopes.indexOf( scope ) === -1 && scopes.indexOf( '*' ) === -1 ) {
return res.status( 403 ).send( { error: 'Invalid scope' } )
}
req.user = { userId: userId, scopes: scopes }
next( )
return next( )
}
}
let authorize = ( req, res, next ) => {
// TODO
// Authorizes the api call against permissions
debug( '🔑 authorization middleware called; yes by default LOL' )
next( )
/*
Authorization: checks user id against access control lists
*/
let roles = { admin: 400, owner: 300, write: 200, read: 100 }
// TODO: Cache results
function authorize( aclTable, resourceTable, requiredRole ) {
let ACL = ( ) => knex( aclTable )
let Resource = ( ) => knex( resourceTable )
return async ( req, res, next ) => {
debug( '🔑 authorization middleware called' )
let { isPublic } = await Resource( ).where( { id: req.params.resourceId } ).select( 'isPublic' ).first( )
if ( isPublic ) return next( )
if ( !req.user ) return res.status( 401 ).send( { error: 'Unauthorized' } )
let [ entry ] = await ACL( ).where( { resource_id: req.params.resourceId, user_id: req.user.id } ).select( '*' )
if ( !entry ) {
return res.status( 401 ).send( { error: 'Unauthorized' } )
}
if ( roles[ entry.role ] >= roles[ requiredRole ] ) {
req.user.role = entry.role
return next( )
} else {
return res.status( 401 ).send( { error: 'Unauthorized' } )
}
}
}
/*
Announcements: orchestrates pushing out events to any subscribers.
(TODO: implement!)
*/
function announce( eventName, eventScope ) {
return async ( req, res, next ) => {
debug( `📣 announce middleware called: ${eventName}:${eventScope}` )
debug( `Event data: ${JSON.stringify( req.eventData )}` )
next( )
}
}
+2 -2
View File
@@ -5,8 +5,8 @@
"main": "index.js",
"scripts": {
"dev": "NODE_ENV=development DEBUG=www:server,speckle:* nodemon ./bin/www --watch . --watch ./bin/www",
"test": "DEBUG=speckle:test NODE_ENV=test mocha -s 0 --exit",
"test-watch": "DEBUG=speckle:test NODE_ENV=test mocha --watch -s 0 --exit"
"test": "DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha -s 0 --exit",
"test-watch": "DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha --watch -s 0 --exit"
},
"author": "",
"license": "MIT",