From 7c0d6e2883abad3ab408d9c5064b56ddaa2e7e98 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Wed, 9 Sep 2020 22:37:49 -0400 Subject: [PATCH] fix: prevent write:groups from self-promoting --- server/core/auth.js | 28 ++++++++++ server/graph/resolvers/group.js | 94 ++++++++++++++++++++++++++++----- 2 files changed, 110 insertions(+), 12 deletions(-) diff --git a/server/core/auth.js b/server/core/auth.js index 56e4ab2c..4d809a6f 100644 --- a/server/core/auth.js +++ b/server/core/auth.js @@ -286,6 +286,34 @@ module.exports = { return false }, + /** + * Check for exclusive permissions (contain any X permission(s) but not any Y permission(s)) + * + * @param {User} user + * @param {Array} includePermissions + * @param {Array} excludePermissions + */ + checkExclusiveAccess(user, includePermissions = [], excludePermissions = []) { + const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions() + + // System Admin + if (userPermissions.includes('manage:system')) { + return true + } + + // Check Inclusion Permissions + if (_.intersection(userPermissions, includePermissions).length < 1) { + return false + } + + // Check Exclusion Permissions + if (_.intersection(userPermissions, excludePermissions).length > 0) { + return false + } + + return true + }, + /** * Check and apply Page Rule specificity * diff --git a/server/graph/resolvers/group.js b/server/graph/resolvers/group.js index fce7d8f2..3fc7db23 100644 --- a/server/graph/resolvers/group.js +++ b/server/graph/resolvers/group.js @@ -1,39 +1,68 @@ const graphHelper = require('../../helpers/graph') const safeRegex = require('safe-regex') const _ = require('lodash') +const gql = require('graphql') /* global WIKI */ -const gql = require('graphql') - module.exports = { Query: { - async groups() { return {} } + async groups () { return {} } }, Mutation: { - async groups() { return {} } + async groups () { return {} } }, GroupQuery: { - async list(obj, args, context, info) { + /** + * FETCH ALL GROUPS + */ + async list () { return WIKI.models.groups.query().select( 'groups.*', WIKI.models.groups.relatedQuery('users').count().as('userCount') ) }, - async single(obj, args, context, info) { + /** + * FETCH A SINGLE GROUP + */ + async single(obj, args) { return WIKI.models.groups.query().findById(args.id) } }, GroupMutation: { - async assignUser(obj, args) { + /** + * ASSIGN USER TO GROUP + */ + async assignUser (obj, args, { req }) { + // Check for guest user + if (args.userId === 2) { + throw new gql.GraphQLError('Cannot assign the Guest user to a group.') + } + + // Check for valid group const grp = await WIKI.models.groups.query().findById(args.groupId) if (!grp) { throw new gql.GraphQLError('Invalid Group ID') } + + // Check assigned permissions for write:groups + if ( + WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) && + grp.permissions.some(p => { + const resType = _.last(p.split(':')) + return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType) + }) + ) { + throw new gql.GraphQLError('You are not authorized to assign a user to this elevated group.') + } + + // Check for valid user const usr = await WIKI.models.users.query().findById(args.userId) if (!usr) { throw new gql.GraphQLError('Invalid User ID') } + + // Check for existing relation const relExist = await WIKI.models.knex('userGroups').where({ userId: args.userId, groupId: args.groupId @@ -41,8 +70,11 @@ module.exports = { if (relExist) { throw new gql.GraphQLError('User is already assigned to group.') } + + // Assign user to group await grp.$relatedQuery('users').relate(usr.id) + // Revoke tokens for this user WIKI.auth.revokeUserTokens({ id: usr.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' }) @@ -50,7 +82,10 @@ module.exports = { responseResult: graphHelper.generateSuccess('User has been assigned to group.') } }, - async create(obj, args) { + /** + * CREATE NEW GROUP + */ + async create (obj, args, { req }) { const group = await WIKI.models.groups.query().insertAndFetch({ name: args.name, permissions: JSON.stringify(WIKI.data.groups.defaultPermissions), @@ -64,7 +99,14 @@ module.exports = { group } }, - async delete(obj, args) { + /** + * DELETE GROUP + */ + async delete (obj, args) { + if (args.id === 1 || args.id === 2) { + throw new gql.GraphQLError('Cannot delete this group.') + } + await WIKI.models.groups.query().deleteById(args.id) WIKI.auth.revokeUserTokens({ id: args.id, kind: 'g' }) @@ -77,7 +119,16 @@ module.exports = { responseResult: graphHelper.generateSuccess('Group has been deleted.') } }, - async unassignUser(obj, args) { + /** + * UNASSIGN USER FROM GROUP + */ + async unassignUser (obj, args) { + if (args.userId === 2) { + throw new gql.GraphQLError('Cannot unassign Guest user') + } + if (args.userId === 1 && args.groupId === 1) { + throw new gql.GraphQLError('Cannot unassign Administrator user from Administrators group.') + } const grp = await WIKI.models.groups.query().findById(args.groupId) if (!grp) { throw new gql.GraphQLError('Invalid Group ID') @@ -95,17 +146,34 @@ module.exports = { responseResult: graphHelper.generateSuccess('User has been unassigned from group.') } }, - async update(obj, args) { + /** + * UPDATE GROUP + */ + async update (obj, args, { req }) { + // Check for unsafe regex page rules if (_.some(args.pageRules, pr => { return pr.match === 'REGEX' && !safeRegex(pr.path) })) { throw new gql.GraphQLError('Some Page Rules contains unsafe or exponential time regex.') } + // Set default redirect on login value if (_.isEmpty(args.redirectOnLogin)) { args.redirectOnLogin = '/' } + // Check assigned permissions for write:groups + if ( + WIKI.auth.checkExclusiveAccess(req.user, ['write:groups'], ['manage:groups', 'manage:system']) && + args.permissions.some(p => { + const resType = _.last(p.split(':')) + return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType) + }) + ) { + throw new gql.GraphQLError('You are not authorized to manage this group or assign these permissions.') + } + + // Update group await WIKI.models.groups.query().patch({ name: args.name, redirectOnLogin: args.redirectOnLogin, @@ -113,9 +181,11 @@ module.exports = { pageRules: JSON.stringify(args.pageRules) }).where('id', args.id) + // Revoke tokens for this group WIKI.auth.revokeUserTokens({ id: args.id, kind: 'g' }) WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'g' }) + // Reload group permissions await WIKI.auth.reloadGroups() WIKI.events.outbound.emit('reloadGroups') @@ -125,7 +195,7 @@ module.exports = { } }, Group: { - users(grp) { + users (grp) { return grp.$relatedQuery('users') } }