feat: banned-users group

This commit is contained in:
gasoved
2020-12-14 09:20:41 +03:00
committed by Julian Lam
parent 389690c3fa
commit 53e0d4d2e0
24 changed files with 350 additions and 114 deletions

View File

@@ -200,7 +200,10 @@ usersAPI.ban = async function (caller, data) {
until: data.until > 0 ? data.until : undefined,
reason: data.reason || undefined,
});
await user.auth.revokeAllSessions(data.uid);
const canLoginIfBanned = await user.bans.canLoginIfBanned(data.uid);
if (!canLoginIfBanned) {
await user.auth.revokeAllSessions(data.uid);
}
};
usersAPI.unban = async function (caller, data) {
@@ -209,6 +212,9 @@ usersAPI.unban = async function (caller, data) {
}
await user.bans.unban(data.uid);
sockets.in('uid_' + data.uid).emit('event:unbanned');
await events.log({
type: 'user-unban',
uid: caller.uid,

View File

@@ -91,7 +91,6 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID) {
});
userData.sso = results.sso.associations;
userData.banned = userData.banned === 1;
userData.website = validator.escape(String(userData.website || ''));
userData.websiteLink = !userData.website.startsWith('http') ? 'http://' + userData.website : userData.website;
userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), '');

View File

@@ -41,7 +41,7 @@ groupsController.get = async function (req, res, next) {
categories.buildForSelectAll(),
]);
if (!group) {
if (!group || groupName === groups.BANNED_USERS) {
return next();
}
group.isOwner = true;
@@ -69,6 +69,7 @@ async function getGroupNames() {
return groupNames.filter(name => name !== 'registered-users' &&
name !== 'verified-users' &&
name !== 'unverified-users' &&
name !== groups.BANNED_USERS &&
!groups.isPrivilegeGroup(name)
);
}

View File

@@ -382,24 +382,25 @@ authenticationController.localLogin = async function (req, username, password, n
const userslug = slugify(username);
const uid = await user.getUidByUserslug(userslug);
try {
const [userData, isAdminOrGlobalMod, banned, hasLoginPrivilege] = await Promise.all([
const [userData, isAdminOrGlobalMod, canLoginIfBanned] = await Promise.all([
user.getUserFields(uid, ['uid', 'passwordExpiry']),
user.isAdminOrGlobalMod(uid),
user.bans.isBanned(uid),
privileges.global.can('local:login', uid),
user.bans.canLoginIfBanned(uid),
]);
userData.isAdminOrGlobalMod = isAdminOrGlobalMod;
if (parseInt(uid, 10) && !hasLoginPrivilege) {
return next(new Error('[[error:local-login-disabled]]'));
}
if (banned) {
if (!canLoginIfBanned) {
const banMesage = await getBanInfo(uid);
return next(new Error(banMesage));
}
// Doing this after the ban check, because user's privileges might change after a ban expires
const hasLoginPrivilege = await privileges.global.can('local:login', uid);
if (parseInt(uid, 10) && !hasLoginPrivilege) {
return next(new Error('[[error:local-login-disabled]]'));
}
const passwordMatch = await user.isPasswordCorrect(uid, password, req.ip);
if (!passwordMatch) {
return next(new Error('[[error:invalid-login-credentials]]'));

View File

@@ -23,6 +23,7 @@ require('./join')(Groups);
require('./leave')(Groups);
require('./cache')(Groups);
Groups.BANNED_USERS = 'banned-users';
Groups.ephemeralGroups = ['guests', 'spiders'];
@@ -30,6 +31,7 @@ Groups.systemGroups = [
'registered-users',
'verified-users',
'unverified-users',
Groups.BANNED_USERS,
'administrators',
'Global Moderators',
];

View File

@@ -90,7 +90,7 @@ module.exports = function (Groups) {
}
async function setGroupTitleIfNotSet(groupNames, uid) {
const ignore = ['registered-users', 'verified-users', 'unverified-users'];
const ignore = ['registered-users', 'verified-users', 'unverified-users', Groups.BANNED_USERS];
groupNames = groupNames.filter(
groupName => !ignore.includes(groupName) && !Groups.isPrivilegeGroup(groupName)
);

View File

@@ -13,7 +13,9 @@ module.exports = function (Groups) {
if (!options.hideEphemeralGroups) {
groupNames = Groups.ephemeralGroups.concat(groupNames);
}
groupNames = groupNames.filter(name => name.toLowerCase().includes(query) && !Groups.isPrivilegeGroup(name));
groupNames = groupNames.filter(name => name.toLowerCase().includes(query) &&
name !== Groups.BANNED_USERS && // hide banned-users in searches
!Groups.isPrivilegeGroup(name));
groupNames = groupNames.slice(0, 100);
let groupsData;

View File

@@ -202,10 +202,6 @@ Messaging.canMessageUser = async (uid, toUid) => {
throw new Error('[[error:no-user]]');
}
const userData = await user.getUserFields(uid, ['banned']);
if (userData.banned) {
throw new Error('[[error:user-banned]]');
}
const canChat = await privileges.global.can('chat', uid);
if (!canChat) {
throw new Error('[[error:no-privileges]]');
@@ -238,10 +234,6 @@ Messaging.canMessageRoom = async (uid, roomId) => {
throw new Error('[[error:not-in-room]]');
}
const userData = await user.getUserFields(uid, ['banned']);
if (userData.banned) {
throw new Error('[[error:user-banned]]');
}
const canChat = await privileges.global.can('chat', uid);
if (!canChat) {
throw new Error('[[error:no-privileges]]');

View File

@@ -32,13 +32,13 @@ const relative_path = nconf.get('relative_path');
middleware.buildHeader = helpers.try(async function buildHeader(req, res, next) {
res.locals.renderHeader = true;
res.locals.isAPI = false;
const [config, isBanned] = await Promise.all([
const [config, canLoginIfBanned] = await Promise.all([
controllers.api.loadConfig(req),
user.bans.isBanned(req.uid),
user.bans.canLoginIfBanned(req.uid),
plugins.hooks.fire('filter:middleware.buildHeader', { req: req, locals: res.locals }),
]);
if (isBanned) {
if (!canLoginIfBanned && req.loggedIn) {
req.logout();
return res.redirect('/');
}

View File

@@ -110,7 +110,7 @@ helpers.getUserPrivileges = async function (cid, userPrivileges) {
});
const members = _.uniq(_.flatten(memberSets));
const memberData = await user.getUsersFields(members, ['picture', 'username']);
const memberData = await user.getUsersFields(members, ['picture', 'username', 'banned']);
memberData.forEach(function (member) {
member.privileges = {};
@@ -133,6 +133,7 @@ helpers.getGroupPrivileges = async function (cid, groupPrivileges) {
let groupNames = allGroupNames.filter(groupName => !groupName.includes(':privileges:') && uniqueGroups.includes(groupName));
groupNames = groups.ephemeralGroups.concat(groupNames);
moveToFront(groupNames, groups.BANNED_USERS);
moveToFront(groupNames, 'Global Moderators');
moveToFront(groupNames, 'unverified-users');
moveToFront(groupNames, 'verified-users');

View File

@@ -18,12 +18,10 @@ User.makeAdmins = async function (socket, uids) {
if (!Array.isArray(uids)) {
throw new Error('[[error:invalid-data]]');
}
const userData = await user.getUsersFields(uids, ['banned']);
userData.forEach((userData) => {
if (userData && userData.banned) {
throw new Error('[[error:cant-make-banned-users-admin]]');
}
});
const isMembersOfBanned = await groups.isMembers(uids, groups.BANNED_USERS);
if (isMembersOfBanned.includes(true)) {
throw new Error('[[error:cant-make-banned-users-admin]]');
}
for (const uid of uids) {
/* eslint-disable no-await-in-loop */
await groups.join('administrators', uid);

View File

@@ -0,0 +1,63 @@
'use strict';
const batch = require('../../batch');
const db = require('../../database');
const groups = require('../../groups');
const now = Date.now();
module.exports = {
name: 'Move banned users to banned-users group',
timestamp: Date.UTC(2020, 11, 13),
method: async function () {
const progress = this.progress;
const timestamp = await db.getObjectField('group:administrators', 'timestamp');
const bannedExists = await groups.exists('banned-users');
if (!bannedExists) {
await groups.create({
name: 'banned-users',
hidden: 1,
private: 1,
system: 1,
disableLeave: 1,
disableJoinRequests: 1,
timestamp: timestamp + 1,
});
}
await batch.processSortedSet('users:banned', async function (uids) {
progress.incr(uids.length);
await db.sortedSetAdd(
'group:banned-users:members',
uids.map(() => now),
uids.map(uid => uid)
);
await db.sortedSetRemove(
[
'group:registered-users:members',
'group:verified-users:members',
'group:unverified-users:members',
'group:Global Moderators:members',
],
uids.map(uid => uid)
);
}, {
batch: 500,
progress: this.progress,
});
const bannedCount = await db.sortedSetCard('group:banned-users:members');
const registeredCount = await db.sortedSetCard('group:registered-users:members');
const verifiedCount = await db.sortedSetCard('group:verified-users:members');
const unverifiedCount = await db.sortedSetCard('group:unverified-users:members');
const globalModCount = await db.sortedSetCard('group:Global Moderators:members');
await db.setObjectField('group:banned-users', 'memberCount', bannedCount);
await db.setObjectField('group:registered-users', 'memberCount', registeredCount);
await db.setObjectField('group:verified-users', 'memberCount', verifiedCount);
await db.setObjectField('group:unverified-users', 'memberCount', unverifiedCount);
await db.setObjectField('group:Global Moderators', 'memberCount', globalModCount);
},
};

View File

@@ -5,10 +5,14 @@ const winston = require('winston');
const meta = require('../meta');
const emailer = require('../emailer');
const db = require('../database');
const groups = require('../groups');
const privileges = require('../privileges');
module.exports = function (User) {
User.bans = {};
const systemGroups = groups.systemGroups.filter(group => group !== groups.BANNED_USERS);
User.bans.ban = async function (uid, until, reason) {
// "until" (optional) is unix timestamp in milliseconds
// "reason" (optional) is a string
@@ -32,7 +36,9 @@ module.exports = function (User) {
banData.reason = reason;
}
await User.setUserField(uid, 'banned', 1);
// Leaving all other system groups to have privileges constrained to the "banned-users" group
await groups.leave(systemGroups, uid);
await groups.join(groups.BANNED_USERS, uid);
await db.sortedSetAdd('users:banned', now, uid);
await db.sortedSetAdd('uid:' + uid + ':bans:timestamp', now, banKey);
await db.setObject(banKey, banData);
@@ -59,10 +65,20 @@ module.exports = function (User) {
};
User.bans.unban = async function (uids) {
if (Array.isArray(uids)) {
await db.setObject(uids.map(uid => 'user:' + uid), { banned: 0, 'banned:expire': 0 });
} else {
await User.setUserFields(uids, { banned: 0, 'banned:expire': 0 });
uids = Array.isArray(uids) ? uids : [uids];
const userData = await User.getUsersFields(uids, ['email:confirmed']);
await db.setObject(uids.map(uid => 'user:' + uid), { 'banned:expire': 0 });
/* eslint-disable no-await-in-loop */
for (const user of userData) {
const systemGroupsToJoin = [
'registered-users',
(parseInt(user['email:confirmed'], 10) === 1 ? 'verified-users' : 'unverified-users'),
];
await groups.leave(groups.BANNED_USERS, user.uid);
// An unbanned user would lost its previous "Global Moderator" status
await groups.join(systemGroupsToJoin, user.uid);
}
await db.sortedSetRemove(['users:banned', 'users:banned:expire'], uids);
@@ -75,22 +91,39 @@ module.exports = function (User) {
return isArray ? result.map(r => r.banned) : result[0].banned;
};
User.bans.canLoginIfBanned = async function (uid) {
let canLogin = true;
const banned = (await User.bans.unbanIfExpired([uid]))[0].banned;
// Group privilege overshadows individual one
if (banned) {
canLogin = await privileges.global.canGroup('local:login', groups.BANNED_USERS);
}
if (banned && !canLogin) {
// Checking a single privilege of user
canLogin = await groups.isMember(uid, 'cid:0:privileges:local:login');
}
return canLogin;
};
User.bans.unbanIfExpired = async function (uids) {
// loading user data will unban if it has expired -barisu
const userData = await User.getUsersFields(uids, ['banned', 'banned:expire']);
const userData = await User.getUsersFields(uids, ['banned:expire']);
return User.bans.calcExpiredFromUserData(userData);
};
User.bans.calcExpiredFromUserData = function (userData) {
User.bans.calcExpiredFromUserData = async function (userData) {
const isArray = Array.isArray(userData);
userData = isArray ? userData : [userData];
userData = userData.map(function (userData) {
userData = await Promise.all(userData.map(async function (userData) {
const banned = await groups.isMember(userData.uid, groups.BANNED_USERS);
return {
banned: userData && !!userData.banned,
banned: banned,
'banned:expire': userData && userData['banned:expire'],
banExpired: userData && userData['banned:expire'] <= Date.now() && userData['banned:expire'] !== 0,
};
});
}));
return isArray ? userData : userData[0];
};

View File

@@ -219,7 +219,8 @@ module.exports = function (User) {
}
if (user.hasOwnProperty('banned') || user.hasOwnProperty('banned:expire')) {
const result = User.bans.calcExpiredFromUserData(user);
const result = await User.bans.calcExpiredFromUserData(user);
user.banned = result.banned;
const unban = result.banned && result.banExpired;
user.banned_until = unban ? 0 : user['banned:expire'];
user.banned_until_readable = user.banned_until && !unban ? utils.toISOString(user.banned_until) : 'Not Banned';

View File

@@ -12,8 +12,6 @@ const utils = require('../utils');
module.exports = function (User) {
const filterFnMap = {
online: user => user.status !== 'offline' && (Date.now() - user.lastonline < 300000),
banned: user => user.banned,
notbanned: user => !user.banned,
flagged: user => parseInt(user.flags, 10) > 0,
verified: user => !!user['email:confirmed'],
unverified: user => !user['email:confirmed'],
@@ -21,8 +19,6 @@ module.exports = function (User) {
const filterFieldMap = {
online: ['status', 'lastonline'],
banned: ['banned'],
notbanned: ['banned'],
flagged: ['flags'],
verified: ['email:confirmed'],
unverified: ['email:confirmed'],
@@ -111,6 +107,12 @@ module.exports = function (User) {
return uids;
}
if (filters.includes('banned') || filters.includes('notbanned')) {
const isMembersOfBanned = await groups.isMembers(uids, groups.BANNED_USERS);
const checkBanned = filters.includes('banned');
uids = uids.filter((uid, index) => (checkBanned ? isMembersOfBanned[index] : !isMembersOfBanned[index]));
}
fields.push('uid');
let userData = await User.getUsersFields(uids, fields);

View File

@@ -30,9 +30,13 @@
<!-- BEGIN privileges.groups -->
<tr data-group-name="{privileges.groups.nameEscaped}" data-private="<!-- IF privileges.groups.isPrivate -->1<!-- ELSE -->0<!-- ENDIF privileges.groups.isPrivate -->">
<td>
<!-- IF privileges.groups.isPrivate -->
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
<!-- ENDIF privileges.groups.isPrivate -->
{{{ if privileges.groups.isPrivate }}}
{{{ if (privileges.groups.name == "banned-users") }}}
<i class="fa fa-exclamation-triangle text-muted" title="[[admin/manage/categories:privileges.inheritance-exception]]"></i>
{{{ else }}}
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
{{{ end }}}
{{{ end }}}
{privileges.groups.name}
</td>
<td>
@@ -109,7 +113,7 @@
</thead>
<tbody>
<!-- BEGIN privileges.users -->
<tr data-uid="{privileges.users.uid}">
<tr data-uid="{privileges.users.uid}"{{{ if privileges.users.banned }}} data-banned{{{ end }}}>
<td>
<!-- IF ../picture -->
<img class="avatar avatar-sm" src="{privileges.users.picture}" title="{privileges.users.username}" />
@@ -117,7 +121,12 @@
<div class="avatar avatar-sm" style="background-color: {../icon:bgColor};">{../icon:text}</div>
<!-- ENDIF ../picture -->
</td>
<td>{privileges.users.username}</td>
<td>
{{{ if privileges.users.banned }}}
<i class="ban fa fa-gavel text-danger" title="[[admin/manage/categories:privileges.banned-user-inheritance]]"></i>
{{{ end }}}
{privileges.users.username}
</td>
<td class="text-center"><input autocomplete="off" type="checkbox" class="checkbox-helper"></td>
{function.spawnPrivilegeStates, privileges.users.username, ../privileges}
</tr>

View File

@@ -13,9 +13,13 @@
<!-- BEGIN privileges.groups -->
<tr data-group-name="{privileges.groups.nameEscaped}" data-private="<!-- IF privileges.groups.isPrivate -->1<!-- ELSE -->0<!-- ENDIF privileges.groups.isPrivate -->">
<td>
<!-- IF privileges.groups.isPrivate -->
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
<!-- ENDIF privileges.groups.isPrivate -->
{{{ if privileges.groups.isPrivate }}}
{{{ if (privileges.groups.name == "banned-users") }}}
<i class="fa fa-exclamation-triangle text-muted" title="[[admin/manage/categories:privileges.inheritance-exception]]"></i>
{{{ else }}}
<i class="fa fa-lock text-muted" title="[[admin/manage/categories:privileges.group-private]]"></i>
{{{ end }}}
{{{ end }}}
{privileges.groups.name}
</td>
<td></td>
@@ -55,7 +59,7 @@
</thead>
<tbody>
<!-- BEGIN privileges.users -->
<tr data-uid="{privileges.users.uid}">
<tr data-uid="{privileges.users.uid}"{{{ if privileges.users.banned }}} data-banned{{{ end }}}>
<td>
<!-- IF ../picture -->
<img class="avatar avatar-sm" src="{privileges.users.picture}" title="{privileges.users.username}" />
@@ -63,7 +67,12 @@
<div class="avatar avatar-sm" style="background-color: {../icon:bgColor};">{../icon:text}</div>
<!-- ENDIF ../picture -->
</td>
<td>{privileges.users.username}</td>
<td>
{{{ if privileges.users.banned }}}
<i class="ban fa fa-gavel text-danger" title="[[admin/manage/categories:privileges.banned-user-inheritance]]"></i>
{{{ end }}}
{privileges.users.username}
</td>
<td class="text-center"><input autocomplete="off" type="checkbox" class="checkbox-helper"></td>
{function.spawnPrivilegeStates, privileges.users.username, ../privileges}
</tr>