diff --git a/app/apollo/index.js b/app/apollo/index.js index e4c59856a..41bd26f90 100644 --- a/app/apollo/index.js +++ b/app/apollo/index.js @@ -63,7 +63,8 @@ const buildCommonApolloContext = async ({ models, req, res, connection, logger } if (connection) { const upgradeReq = connection.context.upgradeReq; const apiKey = connection.context.orgKey; - context = { apiKey: apiKey, req: upgradeReq, req_id: upgradeReq ? upgradeReq.id : undefined, ...context }; + const userToken = connection.context.userToken; + context = { apiKey: apiKey, req: upgradeReq, req_id: upgradeReq ? upgradeReq.id : undefined, userToken, ...context }; } else if (req) { context = { req, req_id: req.id, ...context}; } @@ -100,10 +101,20 @@ const createApolloServer = () => { path: GRAPHQL_PATH, onConnect: async (connectionParams, webSocket, context) => { const req_id = webSocket.upgradeReq.id; + + let orgKey; + if(connectionParams.headers && connectionParams.headers['razee-org-key']) { + orgKey = connectionParams.headers['razee-org-key']; + } + let userToken; + if(connectionParams.headers && connectionParams.headers['userToken']) { + userToken = connectionParams.headers['userToken']; + } + logger.trace({ req_id, connectionParams, context }, 'subscriptions:onConnect'); const me = await models.User.getMeFromConnectionParams( connectionParams, - {req_id, models, logger, ...context}, + {req_id, models, logger, orgKey, userToken, ...context}, ); logger.debug({ me }, 'subscriptions:onConnect upgradeReq getMe'); if (me === undefined) { @@ -111,13 +122,9 @@ const createApolloServer = () => { 'Can not find the session for this subscription request.', ); } - let orgKey; - if(connectionParams.headers && connectionParams.headers['razee-org-key']) { - orgKey = connectionParams.headers['razee-org-key']; - } // add original upgrade request to the context - return { me, upgradeReq: webSocket.upgradeReq, logger, orgKey }; + return { me, upgradeReq: webSocket.upgradeReq, logger, orgKey, userToken }; }, onDisconnect: (webSocket, context) => { logger.debug( diff --git a/app/apollo/models/index.js b/app/apollo/models/index.js index 0589611ae..6b38def9d 100644 --- a/app/apollo/models/index.js +++ b/app/apollo/models/index.js @@ -16,8 +16,11 @@ const bunyan = require('bunyan'); const mongoose = require('mongoose'); +const _ = require('lodash'); -const User = require('./user'); +module.exports = {}; + +const { User } = require('./user'); const Resource = require('./resource'); const ResourceSchema = require('./resource.schema'); const Cluster = require('./cluster'); @@ -147,4 +150,6 @@ async function setupDistributedCollections(mongoUrlsString) { }); } -module.exports = { models, connectDb, setupDistributedCollections, closeDistributedConnections }; +_.assign(module.exports, { + models, connectDb, setupDistributedCollections, closeDistributedConnections, +}); diff --git a/app/apollo/models/user.default.schema.js b/app/apollo/models/user.default.schema.js index 41319cc68..397936a28 100644 --- a/app/apollo/models/user.default.schema.js +++ b/app/apollo/models/user.default.schema.js @@ -135,6 +135,10 @@ UserDefaultSchema.statics.getMeFromConnectionParams = async function( return null; }; +UserDefaultSchema.statics.userTokenIsAuthorized = async function(me, orgId, action, type, attributes, context) { + return this.isAuthorized(me.user, orgId, action, type, attributes, context); +}; + UserDefaultSchema.statics.isAuthorized = async function(me, orgId, action, type, attributes, req_id) { logger.debug({ req_id: req_id },`default isAuthorized ${me} ${action} ${type} ${attributes}`); if (AUTH_MODEL === AUTH_MODELS.DEFAULT) { diff --git a/app/apollo/models/user.js b/app/apollo/models/user.js index 332b0fa0d..3402495b0 100644 --- a/app/apollo/models/user.js +++ b/app/apollo/models/user.js @@ -15,11 +15,75 @@ */ const mongoose = require('mongoose'); +const jwt = require('jsonwebtoken'); +const { AuthenticationError } = require('apollo-server'); const { AUTH_MODEL } = require('./const'); const UserSchema = require(`./user.${AUTH_MODEL}.schema`); const _ = require('lodash'); +const loadMeFromUserToken = async function(userToken, models){ + let obj, userId, orgId; + try { + obj = jwt.decode(userToken); + userId = obj.userId; + orgId = obj.orgId; + }catch(err){ + throw new AuthenticationError('Failed to parse userToken'); + } + if(!userId){ + throw new AuthenticationError('No user id found in userToken'); + } + const user = await this.findById(userId, {}, { lean:true }); + if(!user){ + throw new AuthenticationError('No user found for userToken'); + } + const org = await models.Organization.findById(orgId); + if(!org){ + throw new AuthenticationError('No org found for userToken'); + } + const hasVerifiedToken = _.some(org.orgKeys, (orgKey)=>{ + try{ + jwt.verify(userToken, orgKey); + return true; + } + catch(err){ + return false; + } + }); + if(!hasVerifiedToken){ + throw new AuthenticationError('userToken could not be verified'); + } + return { + type: 'userToken', + user, + }; +}; + +const getMeFromConnectionParamsBase = UserSchema.statics.getMeFromConnectionParams; +UserSchema.statics.getMeFromRequest = async function(...args){ + const [req, {models, req_id, logger}] = args; + const userToken = req.get('userToken'); + + if(userToken){ + return await loadMeFromUserToken.bind(this)(userToken, models); + } + + return await getMeFromConnectionParamsBase.bind(this)(...args); +}; + +const getMeFromRequestBase = UserSchema.statics.getMeFromRequest; +UserSchema.statics.getMeFromRequest = async function(...args){ + const [req, {models, req_id, logger}] = args; + const userToken = req.get('userToken'); + + if(userToken){ + return await loadMeFromUserToken.bind(this)(userToken, models); + } + + return await getMeFromRequestBase.bind(this)(...args); +}; + UserSchema.statics.getBasicUsersByIds = async function(ids){ if(!ids || ids.length < 1){ return []; @@ -39,4 +103,4 @@ UserSchema.statics.getBasicUsersByIds = async function(ids){ const User = mongoose.model('users', UserSchema); -module.exports = User; +module.exports = { User }; diff --git a/app/apollo/models/user.local.schema.js b/app/apollo/models/user.local.schema.js index 75d7cd970..b00e37ded 100644 --- a/app/apollo/models/user.local.schema.js +++ b/app/apollo/models/user.local.schema.js @@ -236,6 +236,12 @@ UserLocalSchema.statics.getMeFromConnectionParams = async function( return null; }; + + +UserLocalSchema.statics.userTokenIsAuthorized = async function(me, orgId, action, type, attributes, context) { + return this.isAuthorized(me.user, orgId, action, type, attributes, context); +}; + UserLocalSchema.statics.isAuthorized = async function(me, orgId, action, type, attributes, context) { const { req_id, logger } = context; logger.debug({ req_id },`local isAuthorized ${me} ${action} ${type} ${attributes}`); diff --git a/app/apollo/models/user.passport.local.schema.js b/app/apollo/models/user.passport.local.schema.js index aebb4458b..e7869ab2d 100644 --- a/app/apollo/models/user.passport.local.schema.js +++ b/app/apollo/models/user.passport.local.schema.js @@ -242,6 +242,10 @@ UserPassportLocalSchema.statics.getMeFromConnectionParams = async function( return null; }; +UserPassportLocalSchema.statics.userTokenIsAuthorized = async function(me, orgId, action, type, attributes, context) { + return this.isAuthorized(me.user, orgId, action, type, attributes, context); +}; + UserPassportLocalSchema.statics.isAuthorized = async function(me, orgId, action, type, attributes, context) { const { req_id, logger } = context; logger.debug({req_id}, `passport.local isAuthorized ${me} ${action} ${type} ${attributes}`); diff --git a/app/apollo/resolvers/common.js b/app/apollo/resolvers/common.js index 96f163a75..be7f08c17 100644 --- a/app/apollo/resolvers/common.js +++ b/app/apollo/resolvers/common.js @@ -26,6 +26,17 @@ const whoIs = me => { // Throw exception if not. const validAuth = async (me, org_id, action, type, queryName, context) => { const {req_id, models, logger} = context; + + if(me && me.type == 'userToken'){ + const result = await models.User.userTokenIsAuthorized(me, org_id, action, type, null, context); + if(!result){ + throw new AuthenticationError( + `You are not allowed to ${action} on ${type} under organization ${org_id} for the query ${queryName}. (using userToken)`, + ); + } + return; + } + if (me === null || !(await models.User.isAuthorized(me, org_id, action, type, null, context))) { logger.error({req_id, me: whoIs(me), org_id, action, type}, `AuthenticationError - ${queryName}`); throw new AuthenticationError(