fix: prevent manage:users users from editing manage:system users

pull/8098/head
NGPixel 1 week ago
parent 8a979690a7
commit 2df962c86d
No known key found for this signature in database

@ -360,6 +360,54 @@ module.exports = {
}) })
}, },
/**
* Check if user (requester) can manage an existing user (target)
*
* Prevents a delegated user manager from taking over / tampering with an
* account holding higher or equal administrative privileges.
*
* @param {User} requester The user attempting to manage the target user
* @param {Number} targetId The ID of the user being managed
* @returns {Boolean}
*/
async checkManageUserTargetAccess(requester, targetId) {
const requesterPermissions = requester.permissions ? requester.permissions : requester.getGlobalPermissions()
// System Admin
if (requesterPermissions.includes('manage:system')) {
return true
}
const target = await WIKI.models.users.query().findById(targetId).withGraphJoined('groups').modifyGraph('groups', builder => {
builder.select('groups.id', 'permissions')
})
if (!target) {
return false
}
// Protect system accounts (root administrator + guest), even if their groups were tampered with
if (target.id <= 2 || target.isSystem) {
return false
}
const targetPermissions = target.getGlobalPermissions()
// Check target for manage:system permission
if (targetPermissions.includes('manage:system')) {
return false
}
// Check target for administrative permissions
if (targetPermissions.some(p => {
const permType = _.last(p.split(':'))
return ['users', 'groups', 'navigation', 'theme', 'api'].includes(permType)
}) && !requesterPermissions.includes('manage:groups')) {
return false
}
return true
},
/** /**
* Check and apply Page Rule specificity * Check and apply Page Rule specificity
* *

@ -130,13 +130,19 @@ module.exports = {
/** /**
* UNASSIGN USER FROM GROUP * UNASSIGN USER FROM GROUP
*/ */
async unassignUser (obj, args) { async unassignUser (obj, args, { req }) {
if (args.userId === 2) { if (args.userId === 2) {
throw new gql.GraphQLError('Cannot unassign Guest user') throw new gql.GraphQLError('Cannot unassign Guest user')
} }
if (args.userId === 1 && args.groupId === 1) { if (args.userId === 1 && args.groupId === 1) {
throw new gql.GraphQLError('Cannot unassign Administrator user from Administrators group.') throw new gql.GraphQLError('Cannot unassign Administrator user from Administrators group.')
} }
// Check that the requester is allowed to manage the target user
if (!(await WIKI.auth.checkManageUserTargetAccess(req.user, args.userId))) {
throw new gql.GraphQLError('You are not authorized to unassign this user from a group.')
}
const grp = await WIKI.models.groups.query().findById(args.groupId) const grp = await WIKI.models.groups.query().findById(args.groupId)
if (!grp) { if (!grp) {
throw new gql.GraphQLError('Invalid Group ID') throw new gql.GraphQLError('Invalid Group ID')

@ -3,6 +3,28 @@ const _ = require('lodash')
/* global WIKI */ /* global WIKI */
/**
* Ensure the requester is allowed to manage the target user and, optionally,
* to assign it to the requested groups.
*/
const assertCanManageTarget = async (requester, targetId, groups) => {
if (!(await WIKI.auth.checkManageUserTargetAccess(requester, targetId))) {
throw new Error('You are not authorized to manage this user.')
}
if (_.isArray(groups) && !(await WIKI.auth.checkAssignUserToGroupAccess(requester, groups))) {
throw new Error('You are not authorized to modify / assign a user from / to an administrative group.')
}
}
/**
* Invalidate all active sessions of the target user
*/
const revokeTargetSessions = targetId => {
WIKI.auth.revokeUserTokens({ id: targetId, kind: 'u' })
WIKI.events.outbound.emit('addAuthRevoke', { id: targetId, kind: 'u' })
}
module.exports = { module.exports = {
Query: { Query: {
async users() { return {} } async users() { return {} }
@ -77,15 +99,16 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async delete (obj, args) { async delete (obj, args, context) {
try { try {
if (args.id <= 2) { if (args.id <= 2) {
throw new WIKI.Error.UserDeleteProtected() throw new WIKI.Error.UserDeleteProtected()
} }
await assertCanManageTarget(context.req.user, args.id)
await WIKI.models.users.deleteUser(args.id, args.replaceId) await WIKI.models.users.deleteUser(args.id, args.replaceId)
WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' }) revokeTargetSessions(args.id)
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
return { return {
responseResult: graphHelper.generateSuccess('User deleted successfully') responseResult: graphHelper.generateSuccess('User deleted successfully')
@ -100,11 +123,19 @@ module.exports = {
}, },
async update (obj, args, context) { async update (obj, args, context) {
try { try {
if (!(await WIKI.auth.checkAssignUserToGroupAccess(context.req.user, args.groups))) { // Prevent locking out the root administrator
throw new Error('You are not authorized to modify / assign a user from / to an administrative group.') if (args.id === 1 && _.isArray(args.groups) && !args.groups.includes(1)) {
throw new Error('Cannot unassign the root administrator from the Administrators group.')
} }
await WIKI.models.users.updateUser(args) await assertCanManageTarget(context.req.user, args.id, args.groups)
const changes = await WIKI.models.users.updateUser(args)
// Invalidate active sessions on security-sensitive changes
if (changes.passwordChanged || changes.groupsChanged) {
revokeTargetSessions(args.id)
}
return { return {
responseResult: graphHelper.generateSuccess('User updated successfully') responseResult: graphHelper.generateSuccess('User updated successfully')
@ -113,8 +144,10 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async verify (obj, args) { async verify (obj, args, context) {
try { try {
await assertCanManageTarget(context.req.user, args.id)
await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id) await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id)
return { return {
@ -124,8 +157,10 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async activate (obj, args) { async activate (obj, args, context) {
try { try {
await assertCanManageTarget(context.req.user, args.id)
await WIKI.models.users.query().patch({ isActive: true }).findById(args.id) await WIKI.models.users.query().patch({ isActive: true }).findById(args.id)
return { return {
@ -135,15 +170,16 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async deactivate (obj, args) { async deactivate (obj, args, context) {
try { try {
if (args.id <= 2) { if (args.id <= 2) {
throw new Error('Cannot deactivate system accounts.') throw new Error('Cannot deactivate system accounts.')
} }
await assertCanManageTarget(context.req.user, args.id)
await WIKI.models.users.query().patch({ isActive: false }).findById(args.id) await WIKI.models.users.query().patch({ isActive: false }).findById(args.id)
WIKI.auth.revokeUserTokens({ id: args.id, kind: 'u' }) revokeTargetSessions(args.id)
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
return { return {
responseResult: graphHelper.generateSuccess('User deactivated successfully') responseResult: graphHelper.generateSuccess('User deactivated successfully')
@ -152,8 +188,10 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async enableTFA (obj, args) { async enableTFA (obj, args, context) {
try { try {
await assertCanManageTarget(context.req.user, args.id)
await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id) await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
return { return {
@ -163,10 +201,14 @@ module.exports = {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async disableTFA (obj, args) { async disableTFA (obj, args, context) {
try { try {
await assertCanManageTarget(context.req.user, args.id)
await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id) await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
revokeTargetSessions(args.id)
return { return {
responseResult: graphHelper.generateSuccess('User 2FA disabled successfully') responseResult: graphHelper.generateSuccess('User 2FA disabled successfully')
} }

@ -677,11 +677,16 @@ module.exports = class User extends Model {
* Update an existing user * Update an existing user
* *
* @param {Object} param0 User ID and fields to update * @param {Object} param0 User ID and fields to update
* @returns {Object} Security-sensitive changes applied to the user
*/ */
static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone, dateFormat, appearance }) { static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone, dateFormat, appearance }) {
const usr = await WIKI.models.users.query().findById(id) const usr = await WIKI.models.users.query().findById(id)
if (usr) { if (usr) {
let usrData = {} let usrData = {}
const changes = {
passwordChanged: false,
groupsChanged: false
}
if (!_.isEmpty(email) && email !== usr.email) { if (!_.isEmpty(email) && email !== usr.email) {
const dupUsr = await WIKI.models.users.query().select('id').where({ const dupUsr = await WIKI.models.users.query().select('id').where({
email, email,
@ -700,6 +705,7 @@ module.exports = class User extends Model {
throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!') throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
} }
usrData.password = newPassword usrData.password = newPassword
changes.passwordChanged = true
} }
if (_.isArray(groups)) { if (_.isArray(groups)) {
const usrGroupsRaw = await usr.$relatedQuery('groups') const usrGroupsRaw = await usr.$relatedQuery('groups')
@ -714,6 +720,7 @@ module.exports = class User extends Model {
for (const grp of remUsrGroups) { for (const grp of remUsrGroups) {
await usr.$relatedQuery('groups').unrelate().where('groupId', grp) await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
} }
changes.groupsChanged = addUsrGroups.length > 0 || remUsrGroups.length > 0
} }
if (!_.isEmpty(location) && location !== usr.location) { if (!_.isEmpty(location) && location !== usr.location) {
usrData.location = _.trim(location) usrData.location = _.trim(location)
@ -731,6 +738,8 @@ module.exports = class User extends Model {
usrData.appearance = appearance usrData.appearance = appearance
} }
await WIKI.models.users.query().patch(usrData).findById(id) await WIKI.models.users.query().patch(usrData).findById(id)
return changes
} else { } else {
throw new WIKI.Error.UserNotFound() throw new WIKI.Error.UserNotFound()
} }

Loading…
Cancel
Save