Merge pull request #1090 from digisomni/feature/expand-kick-functionality

Add improved functionality for `Users.kick` API.
This commit is contained in:
Kalila 2021-03-28 21:05:59 -04:00 committed by GitHub
commit 58fde3c159
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 174 additions and 50 deletions

View file

@ -37,6 +37,7 @@
#include <SettingHandle.h>
#include <SettingHelpers.h>
#include <FingerprintUtils.h>
#include <ModerationFlags.h>
#include "DomainServerNodeData.h"
@ -863,6 +864,20 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
// pull the UUID being kicked from the packet
QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
bool hasOptionalBanParameters = false;
int banParameters;
bool banByUsername;
bool banByFingerprint;
bool banByIP;
// pull optional ban parameters from the packet
if (message.data()->getSize() == (NUM_BYTES_RFC4122_UUID + sizeof(int))) {
hasOptionalBanParameters = true;
message->readPrimitive(&banParameters);
banByUsername = banParameters & ModerationFlags::BanFlags::BAN_BY_USERNAME;
banByFingerprint = banParameters & ModerationFlags::BanFlags::BAN_BY_FINGERPRINT;
banByIP = banParameters & ModerationFlags::BanFlags::BAN_BY_IP;
}
if (!nodeUUID.isNull() && nodeUUID != sendingNode->getUUID()) {
// make sure we actually have a node with this UUID
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
@ -881,16 +896,20 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
if (!verifiedUsername.isEmpty()) {
// if we have a verified user name for this user, we first apply the kick to the username
// check if there were already permissions
bool hadPermissions = havePermissionsForName(verifiedUsername);
// if we have optional ban parameters, we should ban the username based on the parameter
if (!hasOptionalBanParameters || banByUsername) {
// check if there were already permissions
bool hadPermissions = havePermissionsForName(verifiedUsername);
// grab or create permissions for the given username
auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
// grab or create permissions for the given username
auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain);
newPermissions =
!hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain);
// ensure that the connect permission is clear
userPermissions->clear(NodePermissions::Permission::canConnectToDomain);
// ensure that the connect permission is clear
userPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
}
// if we didn't have a username, or this domain-server uses the "multi-kick" setting to
@ -898,7 +917,7 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
// then we remove connect permissions for the machine fingerprint (or IP as fallback)
const QString MULTI_KICK_SETTINGS_KEYPATH = "security.multi_kick_logged_in";
if (verifiedUsername.isEmpty() || valueOrDefaultValueForKeyPath(MULTI_KICK_SETTINGS_KEYPATH).toBool()) {
if (banByFingerprint || verifiedUsername.isEmpty() || valueOrDefaultValueForKeyPath(MULTI_KICK_SETTINGS_KEYPATH).toBool()) {
// remove connect permissions for the machine fingerprint
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
if (nodeData) {
@ -923,36 +942,39 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
fingerprintPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
} else {
// if no node data, all we can do is IP address
auto& kickAddress = matchingNode->getActiveSocket()
? matchingNode->getActiveSocket()->getAddress()
: matchingNode->getPublicSocket().getAddress();
// if no node data, all we can do is ban by IP address
banByIP = true;
}
}
if (banByIP) {
auto& kickAddress = matchingNode->getActiveSocket()
? matchingNode->getActiveSocket()->getAddress()
: matchingNode->getPublicSocket().getAddress();
// probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server
// it is always 127.0.0.1, but looking at the public and local addresses just to be sure
// TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we
// do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it.
if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() ||
kickAddress == limitedNodeList->getLocalSockAddr().getAddress() ||
kickAddress.isLoopback() ) {
qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest";
return;
}
// probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server
// it is always 127.0.0.1, but looking at the public and local addresses just to be sure
// TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we
// do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it.
if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() ||
kickAddress == limitedNodeList->getLocalSockAddr().getAddress() ||
kickAddress.isLoopback() ) {
qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest";
return;
}
NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid());
NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid());
// check if there were already permissions for the IP
bool hadIPPermissions = hasPermissionsForIP(kickAddress);
// check if there were already permissions for the IP
bool hadIPPermissions = hasPermissionsForIP(kickAddress);
// grab or create permissions for the given IP address
auto ipPermissions = _ipPermissions[ipAddressKey];
// grab or create permissions for the given IP address
auto ipPermissions = _ipPermissions[ipAddressKey];
if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
ipPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
ipPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
}

View file

@ -506,6 +506,7 @@ Rectangle {
id: itemCell;
property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore";
property bool isButton: styleData.role === "mute" || styleData.role === "kick";
property bool isBan: styleData.role === "kick";
property bool isAvgAudio: styleData.role === "avgAudioLevel";
opacity: !isButton ? (model && model.isPresent ? 1.0 : 0.4) : 1.0; // Admin actions shouldn't turn gray
@ -605,7 +606,9 @@ Rectangle {
color: 2; // Red
visible: isButton;
enabled: !nameCard.isReplicated;
anchors.centerIn: parent;
anchors.verticalCenter: itemCell.verticalCenter;
anchors.left: parent.left;
anchors.leftMargin: styleData.role === "kick" ? 4 : 18;
width: 32;
height: 32;
onClicked: {
@ -620,7 +623,39 @@ Rectangle {
HiFiGlyphs {
text: (styleData.role === "kick") ? hifi.glyphs.error : hifi.glyphs.muted;
// Size
size: parent.height*1.3;
size: parent.height * 1.3;
// Anchors
anchors.fill: parent;
// Style
horizontalAlignment: Text.AlignHCenter;
color: enabled ? hifi.buttons.textColor[actionButton.color]
: hifi.buttons.disabledTextColor[actionButton.colorScheme];
}
}
HifiControlsUit.Button {
id: hardBanButton;
color: 2; // Red
visible: isBan;
enabled: !nameCard.isReplicated;
anchors.verticalCenter: itemCell.verticalCenter;
anchors.left: parent.left;
anchors.leftMargin: actionButton.width + 14;
width: 32;
height: 32;
onClicked: {
Users[styleData.role](model.sessionId, Users.BAN_BY_USERNAME | Users.BAN_BY_FINGERPRINT | Users.BAN_BY_IP);
UserActivityLogger["palAction"](styleData.role, model.sessionId);
if (styleData.role === "kick") {
nearbyUserModelData.splice(model.userIndex, 1);
nearbyUserModel.remove(model.userIndex); // after changing nearbyUserModelData, b/c ListModel can frob the data
}
}
// muted/error glyphs
HiFiGlyphs {
text: hifi.glyphs.alert;
// Size
size: parent.height * 1.3;
// Anchors
anchors.fill: parent;
// Style

View file

@ -2493,7 +2493,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
return viewFrustum.getPosition();
});
DependencyManager::get<UsersScriptingInterface>()->setKickConfirmationOperator([this] (const QUuid& nodeID) { userKickConfirmation(nodeID); });
DependencyManager::get<UsersScriptingInterface>()->setKickConfirmationOperator([this] (const QUuid& nodeID, unsigned int banFlags) { userKickConfirmation(nodeID, banFlags); });
render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([=](const QString& url, bool htmlContent, QSharedPointer<OffscreenQmlSurface>& webSurface, bool& cachedWebSurface) {
bool isTablet = url == TabletScriptingInterface::QML;
@ -3575,7 +3575,7 @@ void Application::onDesktopRootItemCreated(QQuickItem* rootItem) {
_desktopRootItemCreated = true;
}
void Application::userKickConfirmation(const QUuid& nodeID) {
void Application::userKickConfirmation(const QUuid& nodeID, unsigned int banFlags) {
auto avatarHashMap = DependencyManager::get<AvatarHashMap>();
auto avatar = avatarHashMap->getAvatarBySessionID(nodeID);
@ -3600,7 +3600,7 @@ void Application::userKickConfirmation(const QUuid& nodeID) {
// ask the NodeList to kick the user with the given session ID
if (yes) {
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID);
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID, banFlags);
}
DependencyManager::get<UsersScriptingInterface>()->setWaitForKickResponse(false);

View file

@ -50,6 +50,7 @@
#include <shared/ConicalViewFrustum.h>
#include <shared/FileLogger.h>
#include <RunningMarker.h>
#include <ModerationFlags.h>
#include "avatar/MyAvatar.h"
#include "FancyCamera.h"
@ -608,7 +609,7 @@ private:
void toggleTabletUI(bool shouldOpen = false) const;
bool shouldCaptureMouse() const;
void userKickConfirmation(const QUuid& nodeID);
void userKickConfirmation(const QUuid& nodeID, unsigned int banFlags = ModerationFlags::getDefaultBanFlags());
MainWindow* _window;
QElapsedTimer& _sessionRunTimer;

View file

@ -42,6 +42,7 @@
#include "udt/PacketHeaders.h"
#include "SharedUtil.h"
#include <Trace.h>
#include <ModerationFlags.h>
using namespace std::chrono;
@ -1263,17 +1264,19 @@ float NodeList::getInjectorGain() {
return _injectorGain;
}
void NodeList::kickNodeBySessionID(const QUuid& nodeID) {
void NodeList::kickNodeBySessionID(const QUuid& nodeID, unsigned int banFlags) {
// send a request to domain-server to kick the node with the given session ID
// the domain-server will handle the persistence of the kick (via username or IP)
if (!nodeID.isNull() && getSessionUUID() != nodeID ) {
if (getThisNodeCanKick()) {
// setup the packet
auto kickPacket = NLPacket::create(PacketType::NodeKickRequest, NUM_BYTES_RFC4122_UUID, true);
auto kickPacket = NLPacket::create(PacketType::NodeKickRequest, NUM_BYTES_RFC4122_UUID + sizeof(int), true);
// write the node ID to the packet
kickPacket->write(nodeID.toRfc4122());
// write the ban parameters to the packet
kickPacket->writePrimitive(banFlags);
qCDebug(networking) << "Sending packet to kick node" << uuidStringWithoutCurlyBraces(nodeID);

View file

@ -86,7 +86,7 @@ public:
void setInjectorGain(float gain);
float getInjectorGain();
void kickNodeBySessionID(const QUuid& nodeID);
void kickNodeBySessionID(const QUuid& nodeID, unsigned int banFlags);
void muteNodeBySessionID(const QUuid& nodeID);
void requestUsernameFromSessionID(const QUuid& nodeID);
bool getRequestsDomainListData() { return _requestsDomainListData; }

View file

@ -4,6 +4,7 @@
//
// Created by Ryan Huffman on 6/06/16.
// Copyright 2016 High Fidelity, Inc.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html

View file

@ -4,6 +4,7 @@
//
// Created by Stephen Birarda on 2016-07-11.
// Copyright 2016 High Fidelity, Inc.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@ -51,15 +52,14 @@ float UsersScriptingInterface::getAvatarGain(const QUuid& nodeID) {
return DependencyManager::get<NodeList>()->getAvatarGain(nodeID);
}
void UsersScriptingInterface::kick(const QUuid& nodeID) {
void UsersScriptingInterface::kick(const QUuid& nodeID, unsigned int banFlags) {
if (_kickConfirmationOperator) {
bool waitingForKickResponse = _kickResponseLock.resultWithReadLock<bool>([&] { return _waitingForKickResponse; });
if (getCanKick() && !waitingForKickResponse) {
_kickConfirmationOperator(nodeID);
_kickConfirmationOperator(nodeID, banFlags);
}
} else {
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID);
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID, banFlags);
}
}

View file

@ -4,6 +4,7 @@
//
// Created by Stephen Birarda on 2016-07-11.
// Copyright 2016 High Fidelity, Inc.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
@ -16,6 +17,7 @@
#include <DependencyManager.h>
#include <shared/ReadWriteLockable.h>
#include <ModerationFlags.h>
/**jsdoc
* The <code>Users</code> API provides features to regulate your interaction with other users.
@ -31,6 +33,10 @@
* <code>false</code>. <em>Read-only.</em>
* @property {boolean} requestsDomainListData - <code>true</code> if the client requests extra data from the mixers (such as
* positional data of an avatar they've ignored). <em>Read-only.</em>
* @property {BanFlags} NO_BAN - Do not ban user. <em>Read-only.</em>
* @property {BanFlags} BAN_BY_USERNAME - Ban user by username. <em>Read-only.</em>
* @property {BanFlags} BAN_BY_FINGERPRINT - Ban user by fingerprint. <em>Read-only.</em>
* @property {BanFlags} BAN_BY_IP - Ban user by IP address. <em>Read-only.</em>
*/
class UsersScriptingInterface : public QObject, public Dependency {
Q_OBJECT
@ -39,9 +45,14 @@ class UsersScriptingInterface : public QObject, public Dependency {
Q_PROPERTY(bool canKick READ getCanKick)
Q_PROPERTY(bool requestsDomainListData READ getRequestsDomainListData WRITE setRequestsDomainListData)
Q_PROPERTY(unsigned int NO_BAN READ getNoBan CONSTANT)
Q_PROPERTY(unsigned int BAN_BY_USERNAME READ getBanByUsername CONSTANT)
Q_PROPERTY(unsigned int BAN_BY_FINGERPRINT READ getBanByFingerprint CONSTANT)
Q_PROPERTY(unsigned int BAN_BY_IP READ getBanByIP CONSTANT)
public:
UsersScriptingInterface();
void setKickConfirmationOperator(std::function<void(const QUuid& nodeID)> kickConfirmationOperator) {
void setKickConfirmationOperator(std::function<void(const QUuid& nodeID, unsigned int banFlags)> kickConfirmationOperator) {
_kickConfirmationOperator = kickConfirmationOperator;
}
@ -111,13 +122,14 @@ public slots:
float getAvatarGain(const QUuid& nodeID);
/**jsdoc
* Kicks and bans a user. This removes them from the server and prevents them from returning. The ban is by user name if
* available, or machine fingerprint otherwise.
* Kicks and bans a user. This removes them from the server and prevents them from returning. The ban is by user name (if
* available) and by machine fingerprint. The ban functionality can be controlled with flags.
* <p>This function only works if you're an administrator of the domain you're in.</p>
* @function Users.kick
* @param {Uuid} sessionID - The session ID of the user to kick and ban.
* @param {BanFlags} - Preferred ban flags. <i>Bans a user by username (if available) and machine fingerprint by default.</i>
*/
void kick(const QUuid& nodeID);
void kick(const QUuid& nodeID, unsigned int banFlags = ModerationFlags::getDefaultBanFlags());
/**jsdoc
* Mutes a user's microphone for everyone. The mute is not permanent: the user can unmute themselves.
@ -237,7 +249,12 @@ private:
bool getRequestsDomainListData();
void setRequestsDomainListData(bool requests);
std::function<void(const QUuid& nodeID)> _kickConfirmationOperator;
static constexpr unsigned int getNoBan() { return ModerationFlags::BanFlags::NO_BAN; };
static constexpr unsigned int getBanByUsername() { return ModerationFlags::BanFlags::BAN_BY_USERNAME; };
static constexpr unsigned int getBanByFingerprint() { return ModerationFlags::BanFlags::BAN_BY_FINGERPRINT; };
static constexpr unsigned int getBanByIP() { return ModerationFlags::BanFlags::BAN_BY_IP; };
std::function<void(const QUuid& nodeID, unsigned int banFlags)> _kickConfirmationOperator;
ReadWriteLockable _kickResponseLock;
bool _waitingForKickResponse { false };

View file

@ -0,0 +1,45 @@
//
// ModerationFlags.h
// libraries/shared/src
//
// Created by Kalila L. on Mar 11 2021.
// Copyright 2021 Vircadia contributors.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef vircadia_ModerationFlags_h
#define vircadia_ModerationFlags_h
class ModerationFlags {
public:
/**jsdoc
* <p>A set of flags for moderation ban actions. The value is constructed by using the <code>|</code> (bitwise OR) operator on the
* individual flag values.</p>
* <table>
* <thead>
* <tr><th>Flag Name</th><th>Value</th><th>Description</th></tr>
* </thead>
* <tbody>
* <tr><td>NO_BAN</td><td><code>0</code></td><td>Don't ban user when kicking. <em>This does not currently have an effect.</em></td></tr>
* <tr><td>BAN_BY_USERNAME</td><td><code>1</code></td><td>Ban the person by their username.</td></tr>
* <tr><td>BAN_BY_FINGERPRINT</td><td><code>2</code></td><td>Ban the person by their machine fingerprint.</td></tr>
* <tr><td>BAN_BY_IP</td><td><code>4</code></td><td>Ban the person by their IP address.</td></tr>
* </tbody>
* </table>
* @typedef {number} BanFlags
*/
enum BanFlags
{
NO_BAN = 0,
BAN_BY_USERNAME = 1,
BAN_BY_FINGERPRINT = 2,
BAN_BY_IP = 4
};
static constexpr unsigned int getDefaultBanFlags() { return (BanFlags::BAN_BY_USERNAME | BanFlags::BAN_BY_FINGERPRINT); };
};
#endif // vircadia_ModerationFlags_h