From 0225fcedc0018be79183afee12cdc84f52fb9a51 Mon Sep 17 00:00:00 2001 From: Dimitrie Stefanescu Date: Tue, 21 Apr 2020 00:11:15 +0100 Subject: [PATCH] feat(graphql): scaffoled permission mutations on streams Also, intentionally broke a test on how permissions are handled. --- modules/core/graph/resolvers/streams.js | 25 ++++++++++++++++++++-- modules/core/graph/schemas/streams.graphql | 10 ++++----- modules/core/streams/services.js | 17 ++++++++++----- modules/shared/index.js | 4 ++-- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/modules/core/graph/resolvers/streams.js b/modules/core/graph/resolvers/streams.js index c4b69b5d0..96749b593 100644 --- a/modules/core/graph/resolvers/streams.js +++ b/modules/core/graph/resolvers/streams.js @@ -1,4 +1,5 @@ 'use strict' +const { AuthorizationError, ApolloError } = require( 'apollo-server-express' ) const root = require( 'app-root-path' ) const { createStream, getStream, updateStream, deleteStream, getUserStreams, getStreamUsers, grantPermissionsStream, revokePermissionsStream } = require( '../../streams/services' ) const { validateScopes, authorizeResolver } = require( `${root}/modules/shared` ) @@ -23,8 +24,8 @@ module.exports = { async streamCollection( parent, args, context, info ) { // TODO: Return only the user's public streams if parent.id !== context.userId let streams = await getUserStreams( parent.id ) - // TODO: Implement offsets in service - return { totalCount: streams.length, streams: streams.slice( args.offset, args.offset + args.limit ) } + // TODO: Implement offsets in service, not in friggin array slice + return { totalCount: streams.length, streams: streams.slice( args.offset, args.offset + args.limit ) } } }, Mutation: { @@ -35,10 +36,30 @@ module.exports = { return id }, async streamUpdate( parent, args, context, info ) { + await validateScopes( context.scopes, 'streams:write' ) + await authorizeResolver( context.userId, args.stream.id, 'stream_acl', 'streams', 'owner' ) + return await updateStream( args.stream ) + }, + async streamDelete( parent, args, context, info ) { await validateScopes( context.scopes, 'streams:write' ) await authorizeResolver( context.userId, args.id, 'stream_acl', 'streams', 'owner' ) + return await deleteStream( args.id ) + }, + async streamClone( parent, args, context, info ) { + // TODO + }, + async streamGrantPermission( parent, args, context, info ) { + await validateScopes( context.scopes, 'streams:write' ) + await authorizeResolver( context.userId, args.streamId, 'stream_acl', 'streams', 'owner' ) + if ( context.userId === args.userId ) throw new AuthorizationError( 'You cannot set roles for yourself.' ) + return await grantPermissionsStream( args.streamId, args.userId, args.role.toLowerCase( ) || 'read' ) + }, + async streamRevokePermission( parent, args, context, info ) { + await validateScopes( context.scopes, 'streams:write' ) + await authorizeResolver( context.userId, args.streamId, 'stream_acl', 'streams', 'owner' ) + return await revokePermissionsStream( args.streamId, context.userId ) } } } \ No newline at end of file diff --git a/modules/core/graph/schemas/streams.graphql b/modules/core/graph/schemas/streams.graphql index 2e09a8ada..0f7a8e58f 100644 --- a/modules/core/graph/schemas/streams.graphql +++ b/modules/core/graph/schemas/streams.graphql @@ -38,11 +38,11 @@ enum StreamUserRole { """ Contributors can read and write. """ - CONTRIBUTOR, + WRITE, # CONTRIBUTOR """ Reviewers can view and pull. """ - REVIEWER + READ # REVIEWER } extend type Mutation { @@ -56,17 +56,17 @@ extend type Mutation { """ Grants permissions to an user on a given stream. """ - streamGrantPermissions( streamId: String!, userId: String!, role: StreamUserRole! ): Boolean + streamGrantPermission( streamId: String!, userId: String!, role: StreamUserRole! ): Boolean """ Revokes the permissions of an user on a given stream. """ - streamRevokePermissions( streamId: String!, userId: String! ): Boolean + streamRevokePermission( streamId: String!, userId: String! ): Boolean } input StreamInput { id: String - name: String! + name: String description: String isPublic: Boolean } \ No newline at end of file diff --git a/modules/core/streams/services.js b/modules/core/streams/services.js index d6743c3b2..b9a6c994c 100644 --- a/modules/core/streams/services.js +++ b/modules/core/streams/services.js @@ -37,10 +37,12 @@ module.exports = { }, async grantPermissionsStream( streamId, userId, role ) { - if ( role === 'owner' ) { - let [ ownerAcl ] = await Acl( ).where( { resourceId: streamId, role: 'owner' } ).returning( '*' ).del( ) - await Acl( ).insert( { resourceId: streamId, userId: ownerAcl.userId, role: 'write' } ) - } + // NOTE: allow streams to have more than one owner. + // That's why the code below is commented out. + // if ( role === 'owner' ) { + // let [ ownerAcl ] = await Acl( ).where( { resourceId: streamId, role: 'owner' } ).returning( '*' ).del( ) + // await Acl( ).insert( { resourceId: streamId, userId: ownerAcl.userId, role: 'write' } ) + // } // upsert let query = Acl( ).insert( { userId: userId, resourceId: streamId, role: role } ).toString( ) + ` on conflict on constraint stream_acl_pkey do update set role=excluded.role` @@ -49,8 +51,13 @@ module.exports = { }, async revokePermissionsStream( streamId, userId ) { - let streamAclEntries = Acl( ).where( { resourceId: streamId } ).select( '*' ) + let streamAclEntriesCount = Acl( ).count( { resourceId: streamId } ) + // TODO: check if streamAclEntriesCount === 1 then throw big boo-boo (can't delete last ownership link) + // TODO: below behaviour not correct. Flow: + // Count owners + // If owner count > 1, then proceed to delete, otherwise throw an error (can't delete last owner - delete stream) let delCount = await Acl( ).where( { resourceId: streamId, userId: userId } ).whereNot( { role: 'owner' } ).del( ) + if ( delCount === 0 ) throw new Error( 'Could not revoke permissions for user. Is he an owner?' ) }, diff --git a/modules/shared/index.js b/modules/shared/index.js index 051d6e234..e90f00ebc 100644 --- a/modules/shared/index.js +++ b/modules/shared/index.js @@ -1,5 +1,5 @@ 'use strict' -const { ForbiddenError } = require( 'apollo-server-express' ) +const { ForbiddenError, ApolloError } = require( 'apollo-server-express' ) const debug = require( 'debug' )( 'speckle:middleware' ) const root = require( 'app-root-path' ) const knex = require( `${root}/db/knex` ) @@ -126,7 +126,7 @@ function authorize( aclTable, resourceTable, requiredRole ) { */ -async function authorizeResolver( userId, resourceId, aclTable, resourceTable, role ) { +async function authorizeResolver( userId, resourceId, aclTable, resourceTable, requiredRole ) { let ACL = ( ) => knex( aclTable ) let Resource = ( ) => knex( resourceTable )