diff --git a/modules/core/graph/resolvers/server.js b/modules/core/graph/resolvers/server.js new file mode 100644 index 000000000..925978cbb --- /dev/null +++ b/modules/core/graph/resolvers/server.js @@ -0,0 +1,29 @@ +'use strict' +const root = require( 'app-root-path' ) +const { AuthenticationError, UserInputError } = require( 'apollo-server-express' ) + +const { getAvailableScopes, getAvailableRoles, getServerName, getServerDescription, getAdminContact, getTOS } = require( '../../services/generic' ) + +module.exports = { + Query: { + async serverInfo( parent, args, context, info ) { + let si = { + name: await getServerName( ), + description: await getServerDescription( ), + adminContact: await getAdminContact( ), + tos: await getTOS( ) + } + + return si + } + }, + ServerInfo: { + async roles( parent, args, context, info ) { + return await getAvailableRoles( ) + }, + async scopes( parent, args, context, info ) { + return await getAvailableScopes( ) + } + }, + Mutation: {} +} \ No newline at end of file diff --git a/modules/core/graph/resolvers/user.js b/modules/core/graph/resolvers/user.js index 1c175fc47..7cdb0b20b 100644 --- a/modules/core/graph/resolvers/user.js +++ b/modules/core/graph/resolvers/user.js @@ -34,12 +34,6 @@ module.exports = { } }, Mutation: { - // NOTE: this mutation will not exist, or will be enabled only if local user creation is enabled - async userCreate( parent, args, context, info ) { - let userId = await createUser( args.user ) - let token = await createToken( userId, "Default Token", [ 'streams:read', 'streams:write' ] ) - return token - }, async userEdit( parent, args, context, info ) { await updateUser( context.userId, args.user ) return true diff --git a/modules/core/graph/schemas/server.graphql b/modules/core/graph/schemas/server.graphql new file mode 100644 index 000000000..eb2282c53 --- /dev/null +++ b/modules/core/graph/schemas/server.graphql @@ -0,0 +1,32 @@ +extend type Query { + serverInfo: ServerInfo! +} + +""" +Information about this server. +""" +type ServerInfo { + name: String! + description: String! + adminContact: String! + tos: String! + roles: [Role] + scopes: [Scope] +} + +""" +Available roles. +""" +type Role { + name: String! + description: String! + resourceTarget: String! +} + +""" +Available scopes. +""" +type Scope { + name: String! + description: String! +} \ No newline at end of file diff --git a/modules/core/services/generic.js b/modules/core/services/generic.js index 528415ff2..1e691c181 100644 --- a/modules/core/services/generic.js +++ b/modules/core/services/generic.js @@ -8,14 +8,26 @@ const Scopes = ( ) => knex( 'app_scopes' ) module.exports = { async getAvailableScopes( ) { - return await Roles( ).select( '*' ) - }, - - async getAvailableRoles( ) { return await Scopes( ).select( '*' ) }, - async getServerInfo( ) { - return { todo: true } - } + async getAvailableRoles( ) { + return await Roles( ).select( '*' ) + }, + + async getServerName( ) { + return `TODO: True` + }, + + async getServerDescription( ) { + return `TODO: True` + }, + + async getAdminContact( ) { + return `TODO: True` + }, + + async getTOS( ) { + return `TODO: True` + }, } \ No newline at end of file diff --git a/modules/core/tests/generic.spec.js b/modules/core/tests/generic.spec.js new file mode 100644 index 000000000..411bccf59 --- /dev/null +++ b/modules/core/tests/generic.spec.js @@ -0,0 +1,61 @@ +const chai = require( 'chai' ) +const assert = require( 'assert' ) + +const root = require( 'app-root-path' ) +const { init } = require( `${root}/app` ) +const knex = require( `${root}/db/knex` ) + +const expect = chai.expect + +const { contextApiTokenHelper, validateScopes, authorizeResolver } = require( '../../shared' ) + +describe( 'Generic AuthN & AuthZ controller tests', ( ) => { + + it( 'Validate scopes', async ( ) => { + try { + await validateScopes( ) + assert.fail( 'Should have thrown an error with invalid input' ) + } catch ( e ) { + // + } + + try { + await validateScopes( [ 'a' ], 'b' ) + assert.fail( 'Should have thrown an error' ) + } catch ( e ) { + // + } + + await validateScopes( [ 'a', 'b' ], 'b' ) // should pass + } ) + + it( 'Should create proper context', async ( ) => { + let res = await contextApiTokenHelper( { req: { headers: { authorization: 'Bearer BS' } } } ) + expect( res.auth ).to.equal( false ) + + let res2 = await contextApiTokenHelper( { req: { headers: { authorization: null } } } ) + expect( res2.auth ).to.equal( false ) + + let res3 = await contextApiTokenHelper( { req: { headers: { authorization: undefined } } } ) + expect( res3.auth ).to.equal( false ) + } ) + + it( 'Resolver Authorization Should fail nicely when roles & resources are wanky', async ( ) => { + + try { + let res = await authorizeResolver( null, 'foo', 'bar' ) + assert.fail( 'resolver authorization should have thrown' ) + } catch ( e ) { + + } + + try { + let res = await authorizeResolver( 'foo', 'bar', 'streams:read' ) + assert.fail( 'resolver authorization should have thrown' ) + } catch ( e ) { + + } + + } ) + +} ) \ No newline at end of file diff --git a/modules/core/tests/graph.spec.js b/modules/core/tests/graph.spec.js index 67a177ff8..3e870f3e6 100644 --- a/modules/core/tests/graph.spec.js +++ b/modules/core/tests/graph.spec.js @@ -150,7 +150,7 @@ describe( 'GraphQL API Core', ( ) => { it( 'Should grant some permissions', async ( ) => { const res = await sendRequest( userA.token, { query: `mutation{ streamGrantPermission( streamId: "${ts1}", userId: "${userB.id}" role: "stream:owner") }` } ) - + expect( res ).to.be.json expect( res.body.errors ).to.not.exist expect( res.body.data.streamGrantPermission ).to.equal( true ) @@ -195,7 +195,7 @@ describe( 'GraphQL API Core', ( ) => { const resNotAuth = await sendRequest( userC.token, { query: `query { stream(id:"${ts3}") { id name role } }` } ) expect( resNotAuth ).to.be.json expect( resNotAuth.body.errors ).to.exist - + } ) it( 'Should fail to edit/write on a public stream if no access is provided', async ( ) => { @@ -600,7 +600,43 @@ describe( 'GraphQL API Core', ( ) => { } ) } ) + } ) + describe( 'Server Info', ( ) => { + it( 'Should return a valid server information object', async ( ) => { + let q = ` + query{ + serverInfo{ + name + adminContact + tos + description + roles{ + name + description + resourceTarget + } + scopes{ + name + description + } + } + }` + + let res = await sendRequest( null, { query: q } ) + + expect( res ).to.be.json + expect( res.body.errors ).to.not.exist + expect( res.body.data.serverInfo ).to.be.an( 'object' ) + + let si = res.body.data.serverInfo + expect( si.name ).to.be.a( 'string' ) + expect( si.adminContact ).to.be.a( 'string' ) + expect( si.tos ).to.be.a( 'string' ) + expect( si.description ).to.be.a( 'string' ) + expect( si.roles ).to.be.a( 'array' ) + expect( si.scopes ).to.be.a( 'array' ) + } ) } ) } ) diff --git a/modules/shared/index.js b/modules/shared/index.js index 59469c1cc..16f9933ef 100644 --- a/modules/shared/index.js +++ b/modules/shared/index.js @@ -13,18 +13,22 @@ const { validateToken } = require( `${root}/modules/core/services/tokens` ) async function contextApiTokenHelper( { req, res } ) { // TODO: Cache results for 5 minutes - if ( req.headers.authorization ) { + if ( req.headers.authorization != null ) { + try { + let token = req.headers.authorization.split( ' ' )[ 1 ] - let token = req.headers.authorization.split( ' ' )[ 1 ] + let { valid, scopes, userId } = await validateToken( token ) - let { valid, scopes, userId } = await validateToken( token ) + if ( !valid ) { + return { auth: false } + } - if ( !valid ) { - return { auth: false } + return { auth: true, userId, token, scopes } + } catch ( e ) { + return { auth: false, err: e } } - - return { auth: true, userId, token, scopes } } + return { auth: false } } /* diff --git a/package.json b/package.json index 5cae97162..08d52f068 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test nyc nyc --reporter=html mocha -s 0 --exit", "test-watch": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha --watch -s 0 --exit", "test-graph": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha ./modules/core/tests/graph.spec.js --watch -s 0 --exit --no-config", + "test-generic": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha ./modules/core/tests/generic.spec.js --watch -s 0 --exit --no-config", "test-objects": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha ./modules/core/tests/objects.spec.js --watch -s 0 --exit --no-config", "test-streams": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha ./modules/core/tests/streams.spec.js --watch -s 0 --exit --no-config", "test-references": "PORT=3001 DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha ./modules/core/tests/references.spec.js --watch -s 0 --exit --no-config",