//// DomainGatekeeper.cpp// domain-server/src//// Created by Stephen Birarda on 2015-08-24.// Copyright 2015 High Fidelity, Inc.// Copyright 2020 Vircadia contributors.// Copyright 2023 Overte e.V.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "DomainGatekeeper.h" #include <openssl/err.h>#include <openssl/rsa.h>#include <openssl/x509.h>#include <random> #include <QtCore/QDataStream>#include <QtCore/QMetaMethod> #include <AccountManager.h>#include <Assignment.h> #include "DomainServer.h"#include "DomainServerNodeData.h"#include "WarningsSuppression.h" using SharedAssignmentPointer = QSharedPointer<Assignment>; DomainGatekeeper::DomainGatekeeper(DomainServer* server) : _server(server){ initLocalIDManagement();} void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID, const QString& nodeVersion) { _pendingAssignedNodes.emplace(std::piecewise_construct, std::forward_as_tuple(nodeUUID), std::forward_as_tuple(assignmentUUID, nodeVersion));} QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID) { auto it = _pendingAssignedNodes.find(tempUUID); if (it != _pendingAssignedNodes.end()) { return it->second.getAssignmentUUID(); } else { return QUuid(); }} const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer; void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessage> message) { if (message->getSize() == 0) { return; } QDataStream packetStream(message->getMessage()); // read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it NodeConnectionData nodeConnection = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr()); QByteArray myProtocolVersion = protocolVersionsSignature(); if (nodeConnection.protocolVersion != myProtocolVersion) { sendProtocolMismatchConnectionDenial(message->getSenderSockAddr()); return; } if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) { qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection."; return; } static const NodeSet VALID_NODE_TYPES { NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer, NodeType::EntityScriptServer }; if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) { qDebug() << "Received an invalid node type with connect request. Will not allow connection from" << nodeConnection.senderSockAddr << ": " << nodeConnection.nodeType; return; } // check if this connect request matches an assignment in the queue auto pendingAssignment = _pendingAssignedNodes.find(nodeConnection.connectUUID); SharedNodePointer node; QString username; if (pendingAssignment != _pendingAssignedNodes.end()) { node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QByteArray usernameSignature; QString domainUsername; QStringList domainTokens; if (message->getBytesLeftToRead() > 0) { // read username from packet packetStream >> username; if (message->getBytesLeftToRead() > 0) { // read user signature from packet packetStream >> usernameSignature; if (message->getBytesLeftToRead() > 0) { // Read domain username from packet. packetStream >> domainUsername; domainUsername = domainUsername.toLower(); // Domain usernames are case-insensitive; internally lower-case. if (message->getBytesLeftToRead() > 0) { // Read domain tokens from packet. QString domainTokensString; packetStream >> domainTokensString; domainTokens = domainTokensString.split(":"); } } } } node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainTokens.value(0), domainTokens.value(1)); } if (node) { // set the sending sock addr and node interest set on this node DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData()); nodeData->setSendingSockAddr(message->getSenderSockAddr()); // guard against patched agents asking to hear about other agents auto safeInterestSet = QSet<NodeType_t>(nodeConnection.interestList.begin(), nodeConnection.interestList.end()); if (nodeConnection.nodeType == NodeType::Agent) { safeInterestSet.remove(NodeType::Agent); } nodeData->setNodeInterestSet(safeInterestSet); nodeData->setPlaceName(nodeConnection.placeName); QMetaEnum metaEnum = QMetaEnum::fromType<LimitedNodeList::ConnectReason>(); qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID()) << "on" << message->getSenderSockAddr() << "and machine fingerprint" << nodeConnection.machineFingerprint << "user" << username << "reason" << QString(metaEnum.valueToKey(nodeConnection.connectReason)) << "previous connection uptime" << nodeConnection.previousConnectionUpTime/USECS_PER_MSEC << "msec"; // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away emit connectedNode(node, message->getFirstPacketReceiveTime()); } else { qDebug() << "Refusing connection from node at" << message->getSenderSockAddr() << "and machine fingerprint" << nodeConnection.machineFingerprint; }} NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, QString verifiedDomainUserName, const QHostAddress& senderAddress, const QUuid& machineFingerprint) { NodePermissions userPerms; userPerms.setAll(false); if (isLocalUser) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);#ifdef WANT_DEBUG qDebug() << "| user-permissions: is local user, so:" << userPerms;#endif } // If this user is a known member of a domain group, give them the implied permissions. // Do before processing verifiedUsername in case user is logged into the Directory Services and is a member of a blocklist group. if (!verifiedDomainUserName.isEmpty()) { auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; foreach (QString userGroup, userGroups) { // A domain group is signified by a leading special character, "@". // Multiple domain groups may be specified in one domain server setting as a comma- and/or space-separated lists of // domain group names. For example, "@silver @Gold, @platinum". auto domainGroups = _server->_settingsManager.getDomainServerGroupNames() .filter(QRegularExpression("^(.*[\\s,])?" + QRegularExpression::escape(userGroup) + "([\\s,].*)?$", QRegularExpression::CaseInsensitiveOption)); foreach(QString domainGroup, domainGroups) { userPerms |= _server->_settingsManager.getPermissionsForGroup(domainGroup, QUuid()); // No rank for domain groups.#ifdef WANT_DEBUG qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << domainGroup << "so:" << userPerms;#endif } } } if (verifiedUsername.isEmpty()) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);#ifdef WANT_DEBUG qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms;#endif if (_server->_settingsManager.hasPermissionsForMachineFingerprint(machineFingerprint)) { userPerms = _server->_settingsManager.getPermissionsForMachineFingerprint(machineFingerprint);#ifdef WANT_DEBUG qDebug() << "| user-permissions: specific Machine Fingerprint matches, so: " << userPerms;#endif } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific IP matches, so:" << userPerms;#endif } } else { if (_server->_settingsManager.havePermissionsForName(verifiedUsername)) { userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername);#ifdef WANT_DEBUG qDebug() << "| user-permissions: specific user matches, so:" << userPerms;#endif } else if (_server->_settingsManager.hasPermissionsForMachineFingerprint(machineFingerprint)) { userPerms = _server->_settingsManager.getPermissionsForMachineFingerprint(machineFingerprint);#ifdef WANT_DEBUG qDebug() << "| user-permissions: specific Machine Fingerprint matches, so: " << userPerms;#endif } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific IP matches, so:" << userPerms;#endif } else { // they are logged into Directory Services, but we don't have specific permissions for them. userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);#ifdef WANT_DEBUG qDebug() << "| user-permissions: user is logged-into Directory Services, so:" << userPerms;#endif // if this user is a friend of the domain-owner, give them friend's permissions if (_domainOwnerFriends.contains(verifiedUsername)) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameFriends);#ifdef WANT_DEBUG qDebug() << "| user-permissions: user is friends with domain-owner, so:" << userPerms;#endif } // if this user is a known member of a group, give them the implied permissions foreach (QUuid groupID, _server->_settingsManager.getGroupIDs()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { userPerms |= _server->_settingsManager.getPermissionsForGroup(groupID, rankID); GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID);#ifdef WANT_DEBUG qDebug() << "| user-permissions: user " << verifiedUsername << "is in group:" << groupID << " rank:" << rank.name << "so:" << userPerms;#endif } } // if this user is a known member of a blocklist group, remove the implied permissions foreach (QUuid groupID, _server->_settingsManager.getBlocklistGroupIDs()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { userPerms &= ~_server->_settingsManager.getForbiddensForGroup(groupID, rankID); GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID);#ifdef WANT_DEBUG qDebug() << "| user-permissions: user is in blocklist group:" << groupID << " rank:" << rank.name << "so:" << userPerms;#endif } } } } userPerms.setID(verifiedUsername); userPerms.setVerifiedUserName(verifiedUsername); } // If this user is a known member of an domain group that is blocklisted, remove the implied permissions. if (!verifiedDomainUserName.isEmpty()) { auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; foreach(QString userGroup, userGroups) { // A domain group is signified by a leading special character, "@". // Multiple domain groups may be specified in one domain server setting as a comma- and/or space-separated lists of // domain group names. For example, "@silver @Gold, @platinum". auto domainGroups = _server->_settingsManager.getDomainServerBlocklistGroupNames() .filter(QRegularExpression("^(.*[\\s,])?" + QRegularExpression::escape(userGroup) + "([\\s,].*)?$", QRegularExpression::CaseInsensitiveOption)); foreach(QString domainGroup, domainGroups) { userPerms &= ~_server->_settingsManager.getForbiddensForGroup(domainGroup, QUuid());#ifdef WANT_DEBUG qDebug() << "| user-permissions: domain user is in blocklist group:" << domainGroup << "so:" << userPerms;#endif } } userPerms.setVerifiedDomainUserName(verifiedDomainUserName); } #ifdef WANT_DEBUG qDebug() << "| user-permissions: final:" << userPerms;#endif return userPerms;} void DomainGatekeeper::updateNodePermissions() { // If the permissions were changed on the domain-server webpage (and nothing else was), a restart isn't required -- // we reprocess the permissions map and update the nodes here. The node list is frequently sent out to all // the connected nodes, so these changes are propagated to other nodes. QList<SharedNodePointer> nodesToKill; auto limitedNodeList = DependencyManager::get<LimitedNodeList>(); QWeakPointer<LimitedNodeList> limitedNodeListWeak = limitedNodeList; limitedNodeList->eachNode([this, limitedNodeListWeak, &nodesToKill](const SharedNodePointer& node){ // the id and the username in NodePermissions will often be the same, but id is set before // authentication and verifiedUsername is only set once they user's key has been confirmed. QString verifiedUsername = node->getPermissions().getVerifiedUserName(); QString verifiedDomainUserName = node->getPermissions().getVerifiedDomainUserName(); NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0)); if (node->getPermissions().isAssignment) { // this node is an assignment-client userPerms.isAssignment = true; userPerms.permissions |= NodePermissions::Permission::canConnectToDomain; userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData; userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities; userPerms.permissions |= NodePermissions::Permission::canViewAssetURLs; } else { // at this point we don't have a sending socket for packets from this node - assume it is the active socket // or the public socket if we haven't activated a socket for the node yet SockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket(); QUuid machineFingerprint; bool isLocalUser { false }; DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData()); if (nodeData) { machineFingerprint = nodeData->getMachineFingerprint(); auto sendingAddress = nodeData->getSendingSockAddr().getAddress(); auto nodeList = limitedNodeListWeak.lock(); isLocalUser = ((nodeList && sendingAddress == nodeList->getLocalSockAddr().getAddress()) || sendingAddress == QHostAddress::LocalHost); } userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUserName, connectingAddr.getAddress(), machineFingerprint); } node->setPermissions(userPerms); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { qDebug() << "node" << node->getUUID() << "no longer has permission to connect."; // hang up on this node nodesToKill << node; } }); foreach (auto node, nodesToKill) { emit killNode(node); }} SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeConnectionData& nodeConnection, const PendingAssignedNodeData& pendingAssignment) { // make sure this matches an assignment the DS told us we sent out auto it = _pendingAssignedNodes.find(nodeConnection.connectUUID); SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer(); if (it != _pendingAssignedNodes.end()) { // find the matching queued static assignment in DS queue matchingQueuedAssignment = _server->dequeueMatchingAssignment(it->second.getAssignmentUUID(), nodeConnection.nodeType); if (matchingQueuedAssignment) { qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID) << "matches unfulfilled assignment" << uuidStringWithoutCurlyBraces(matchingQueuedAssignment->getUUID()); } else { // this is a node connecting to fulfill an assignment that doesn't exist // don't reply back to them so they cycle back and re-request an assignment qDebug() << "No match for assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID); return SharedNodePointer(); } } else { qDebug() << "No assignment was deployed with UUID" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID); return SharedNodePointer(); } // add the new node SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(newNode->getLinkedData()); // set assignment related data on the linked data for this node nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); nodeData->setMachineFingerprint(nodeConnection.machineFingerprint); // client-side send time of last connect/domain list request nodeData->setLastDomainCheckinTimestamp(nodeConnection.lastPingTimestamp); nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting _pendingAssignedNodes.erase(it); NodePermissions userPerms; userPerms.isAssignment = true; userPerms.permissions |= NodePermissions::Permission::canConnectToDomain; // always allow assignment clients to create and destroy entities userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData; userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities; userPerms.permissions |= NodePermissions::Permission::canViewAssetURLs; newNode->setPermissions(userPerms); return newNode;} const QString AUTHENTICATION_ENABLE_OAUTH2 = "authentication.enable_oauth2";const QString AUTHENTICATION_OAUTH2_URL_PATH = "authentication.oauth2_url_path";const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base";const QString AUTHENTICATION_PLUGIN_CLIENT_ID = "authentication.plugin_client_id";const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity";const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, const QString& domainAccessToken, const QString& domainRefreshToken) { auto limitedNodeList = DependencyManager::get<LimitedNodeList>(); // start with empty permissions NodePermissions userPerms(NodePermissionsKey(username, 0)); userPerms.setAll(false); // check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress(); bool isLocalUser = (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); QString verifiedUsername; // if this remains empty, consider this an anonymous connection attempt if (!username.isEmpty()) { const QUuid& connectionToken = _connectionTokenHash.value(username.toLower()); if (usernameSignature.isEmpty() || connectionToken.isNull()) { // user is attempting to prove their identity to us, but we don't have enough information sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); // ask for their public key right now to make sure we have it requestUserPublicKey(username, true); getGroupMemberships(username); // optimistically get started on group memberships#ifdef WANT_DEBUG qDebug() << "stalling login because we have no username-signature:" << username;#endif if (!domainHasLogin() || domainUsername.isEmpty()) { return SharedNodePointer(); } } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they sent us a username and the signature verifies it getGroupMemberships(username); verifiedUsername = username.toLower(); } else { // they sent us a username, but it didn't check out requestUserPublicKey(username);#ifdef WANT_DEBUG qDebug() << "stalling login because signature verification failed:" << username;#endif if (!domainHasLogin() || domainUsername.isEmpty()) { return SharedNodePointer(); } } } // The domain may have its own users and groups. QString verifiedDomainUsername; QStringList verifiedDomainUserGroups; if (domainHasLogin() && !domainUsername.isEmpty()) { if (domainAccessToken.isEmpty()) { // User is attempting to prove their domain identity.#ifdef WANT_DEBUG qDebug() << "Stalling login because we have no domain OAuth2 tokens:" << domainUsername;Lines 1–500 of 1284. Load more
// // DomainGatekeeper.cpp // domain-server/src // // Created by Stephen Birarda on 2015-08-24. // Copyright 2015 High Fidelity, Inc. // Copyright 2020 Vircadia contributors. // Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "DomainGatekeeper.h" #include <openssl/err.h> #include <openssl/rsa.h> #include <openssl/x509.h> #include <random> #include <QtCore/QDataStream> #include <QtCore/QMetaMethod> #include <AccountManager.h> #include <Assignment.h> #include "DomainServer.h" #include "DomainServerNodeData.h" #include "WarningsSuppression.h" using SharedAssignmentPointer = QSharedPointer<Assignment>; DomainGatekeeper::DomainGatekeeper(DomainServer* server) : _server(server) { initLocalIDManagement(); } void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID, const QString& nodeVersion) { _pendingAssignedNodes.emplace(std::piecewise_construct, std::forward_as_tuple(nodeUUID), std::forward_as_tuple(assignmentUUID, nodeVersion)); } QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID) { auto it = _pendingAssignedNodes.find(tempUUID); if (it != _pendingAssignedNodes.end()) { return it->second.getAssignmentUUID(); } else { return QUuid(); } } const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer; void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessage> message) { if (message->getSize() == 0) { return; } QDataStream packetStream(message->getMessage()); // read a NodeConnectionData object from the packet so we can pass around this data while we're inspecting it NodeConnectionData nodeConnection = NodeConnectionData::fromDataStream(packetStream, message->getSenderSockAddr()); QByteArray myProtocolVersion = protocolVersionsSignature(); if (nodeConnection.protocolVersion != myProtocolVersion) { sendProtocolMismatchConnectionDenial(message->getSenderSockAddr()); return; } if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) { qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection."; return; } static const NodeSet VALID_NODE_TYPES { NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer, NodeType::EntityScriptServer }; if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) { qDebug() << "Received an invalid node type with connect request. Will not allow connection from" << nodeConnection.senderSockAddr << ": " << nodeConnection.nodeType; return; } // check if this connect request matches an assignment in the queue auto pendingAssignment = _pendingAssignedNodes.find(nodeConnection.connectUUID); SharedNodePointer node; QString username; if (pendingAssignment != _pendingAssignedNodes.end()) { node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QByteArray usernameSignature; QString domainUsername; QStringList domainTokens; if (message->getBytesLeftToRead() > 0) { // read username from packet packetStream >> username; if (message->getBytesLeftToRead() > 0) { // read user signature from packet packetStream >> usernameSignature; if (message->getBytesLeftToRead() > 0) { // Read domain username from packet. packetStream >> domainUsername; domainUsername = domainUsername.toLower(); // Domain usernames are case-insensitive; internally lower-case. if (message->getBytesLeftToRead() > 0) { // Read domain tokens from packet. QString domainTokensString; packetStream >> domainTokensString; domainTokens = domainTokensString.split(":"); } } } } node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainTokens.value(0), domainTokens.value(1)); } if (node) { // set the sending sock addr and node interest set on this node DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData()); nodeData->setSendingSockAddr(message->getSenderSockAddr()); // guard against patched agents asking to hear about other agents auto safeInterestSet = QSet<NodeType_t>(nodeConnection.interestList.begin(), nodeConnection.interestList.end()); if (nodeConnection.nodeType == NodeType::Agent) { safeInterestSet.remove(NodeType::Agent); } nodeData->setNodeInterestSet(safeInterestSet); nodeData->setPlaceName(nodeConnection.placeName); QMetaEnum metaEnum = QMetaEnum::fromType<LimitedNodeList::ConnectReason>(); qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID()) << "on" << message->getSenderSockAddr() << "and machine fingerprint" << nodeConnection.machineFingerprint << "user" << username << "reason" << QString(metaEnum.valueToKey(nodeConnection.connectReason)) << "previous connection uptime" << nodeConnection.previousConnectionUpTime/USECS_PER_MSEC << "msec"; // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away emit connectedNode(node, message->getFirstPacketReceiveTime()); } else { qDebug() << "Refusing connection from node at" << message->getSenderSockAddr() << "and machine fingerprint" << nodeConnection.machineFingerprint; } } NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, QString verifiedDomainUserName, const QHostAddress& senderAddress, const QUuid& machineFingerprint) { NodePermissions userPerms; userPerms.setAll(false); if (isLocalUser) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost); #ifdef WANT_DEBUG qDebug() << "| user-permissions: is local user, so:" << userPerms; #endif } // If this user is a known member of a domain group, give them the implied permissions. // Do before processing verifiedUsername in case user is logged into the Directory Services and is a member of a blocklist group. if (!verifiedDomainUserName.isEmpty()) { auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; foreach (QString userGroup, userGroups) { // A domain group is signified by a leading special character, "@". // Multiple domain groups may be specified in one domain server setting as a comma- and/or space-separated lists of // domain group names. For example, "@silver @Gold, @platinum". auto domainGroups = _server->_settingsManager.getDomainServerGroupNames() .filter(QRegularExpression("^(.*[\\s,])?" + QRegularExpression::escape(userGroup) + "([\\s,].*)?$", QRegularExpression::CaseInsensitiveOption)); foreach(QString domainGroup, domainGroups) { userPerms |= _server->_settingsManager.getPermissionsForGroup(domainGroup, QUuid()); // No rank for domain groups. #ifdef WANT_DEBUG qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << domainGroup << "so:" << userPerms; #endif } } } if (verifiedUsername.isEmpty()) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous); #ifdef WANT_DEBUG qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms; #endif if (_server->_settingsManager.hasPermissionsForMachineFingerprint(machineFingerprint)) { userPerms = _server->_settingsManager.getPermissionsForMachineFingerprint(machineFingerprint); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific Machine Fingerprint matches, so: " << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific IP matches, so:" << userPerms; #endif } } else { if (_server->_settingsManager.havePermissionsForName(verifiedUsername)) { userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific user matches, so:" << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForMachineFingerprint(machineFingerprint)) { userPerms = _server->_settingsManager.getPermissionsForMachineFingerprint(machineFingerprint); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific Machine Fingerprint matches, so: " << userPerms; #endif } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { // this user comes from an IP we have in our permissions table, apply those permissions userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific IP matches, so:" << userPerms; #endif } else { // they are logged into Directory Services, but we don't have specific permissions for them. userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn); #ifdef WANT_DEBUG qDebug() << "| user-permissions: user is logged-into Directory Services, so:" << userPerms; #endif // if this user is a friend of the domain-owner, give them friend's permissions if (_domainOwnerFriends.contains(verifiedUsername)) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameFriends); #ifdef WANT_DEBUG qDebug() << "| user-permissions: user is friends with domain-owner, so:" << userPerms; #endif } // if this user is a known member of a group, give them the implied permissions foreach (QUuid groupID, _server->_settingsManager.getGroupIDs()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { userPerms |= _server->_settingsManager.getPermissionsForGroup(groupID, rankID); GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID); #ifdef WANT_DEBUG qDebug() << "| user-permissions: user " << verifiedUsername << "is in group:" << groupID << " rank:" << rank.name << "so:" << userPerms; #endif } } // if this user is a known member of a blocklist group, remove the implied permissions foreach (QUuid groupID, _server->_settingsManager.getBlocklistGroupIDs()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); if (rankID != QUuid()) { userPerms &= ~_server->_settingsManager.getForbiddensForGroup(groupID, rankID); GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID); #ifdef WANT_DEBUG qDebug() << "| user-permissions: user is in blocklist group:" << groupID << " rank:" << rank.name << "so:" << userPerms; #endif } } } } userPerms.setID(verifiedUsername); userPerms.setVerifiedUserName(verifiedUsername); } // If this user is a known member of an domain group that is blocklisted, remove the implied permissions. if (!verifiedDomainUserName.isEmpty()) { auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; foreach(QString userGroup, userGroups) { // A domain group is signified by a leading special character, "@". // Multiple domain groups may be specified in one domain server setting as a comma- and/or space-separated lists of // domain group names. For example, "@silver @Gold, @platinum". auto domainGroups = _server->_settingsManager.getDomainServerBlocklistGroupNames() .filter(QRegularExpression("^(.*[\\s,])?" + QRegularExpression::escape(userGroup) + "([\\s,].*)?$", QRegularExpression::CaseInsensitiveOption)); foreach(QString domainGroup, domainGroups) { userPerms &= ~_server->_settingsManager.getForbiddensForGroup(domainGroup, QUuid()); #ifdef WANT_DEBUG qDebug() << "| user-permissions: domain user is in blocklist group:" << domainGroup << "so:" << userPerms; #endif } } userPerms.setVerifiedDomainUserName(verifiedDomainUserName); } #ifdef WANT_DEBUG qDebug() << "| user-permissions: final:" << userPerms; #endif return userPerms; } void DomainGatekeeper::updateNodePermissions() { // If the permissions were changed on the domain-server webpage (and nothing else was), a restart isn't required -- // we reprocess the permissions map and update the nodes here. The node list is frequently sent out to all // the connected nodes, so these changes are propagated to other nodes. QList<SharedNodePointer> nodesToKill; auto limitedNodeList = DependencyManager::get<LimitedNodeList>(); QWeakPointer<LimitedNodeList> limitedNodeListWeak = limitedNodeList; limitedNodeList->eachNode([this, limitedNodeListWeak, &nodesToKill](const SharedNodePointer& node){ // the id and the username in NodePermissions will often be the same, but id is set before // authentication and verifiedUsername is only set once they user's key has been confirmed. QString verifiedUsername = node->getPermissions().getVerifiedUserName(); QString verifiedDomainUserName = node->getPermissions().getVerifiedDomainUserName(); NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0)); if (node->getPermissions().isAssignment) { // this node is an assignment-client userPerms.isAssignment = true; userPerms.permissions |= NodePermissions::Permission::canConnectToDomain; userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData; userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities; userPerms.permissions |= NodePermissions::Permission::canViewAssetURLs; } else { // at this point we don't have a sending socket for packets from this node - assume it is the active socket // or the public socket if we haven't activated a socket for the node yet SockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket(); QUuid machineFingerprint; bool isLocalUser { false }; DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData()); if (nodeData) { machineFingerprint = nodeData->getMachineFingerprint(); auto sendingAddress = nodeData->getSendingSockAddr().getAddress(); auto nodeList = limitedNodeListWeak.lock(); isLocalUser = ((nodeList && sendingAddress == nodeList->getLocalSockAddr().getAddress()) || sendingAddress == QHostAddress::LocalHost); } userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUserName, connectingAddr.getAddress(), machineFingerprint); } node->setPermissions(userPerms); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { qDebug() << "node" << node->getUUID() << "no longer has permission to connect."; // hang up on this node nodesToKill << node; } }); foreach (auto node, nodesToKill) { emit killNode(node); } } SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeConnectionData& nodeConnection, const PendingAssignedNodeData& pendingAssignment) { // make sure this matches an assignment the DS told us we sent out auto it = _pendingAssignedNodes.find(nodeConnection.connectUUID); SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer(); if (it != _pendingAssignedNodes.end()) { // find the matching queued static assignment in DS queue matchingQueuedAssignment = _server->dequeueMatchingAssignment(it->second.getAssignmentUUID(), nodeConnection.nodeType); if (matchingQueuedAssignment) { qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID) << "matches unfulfilled assignment" << uuidStringWithoutCurlyBraces(matchingQueuedAssignment->getUUID()); } else { // this is a node connecting to fulfill an assignment that doesn't exist // don't reply back to them so they cycle back and re-request an assignment qDebug() << "No match for assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID); return SharedNodePointer(); } } else { qDebug() << "No assignment was deployed with UUID" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID); return SharedNodePointer(); } // add the new node SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(newNode->getLinkedData()); // set assignment related data on the linked data for this node nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); nodeData->setMachineFingerprint(nodeConnection.machineFingerprint); // client-side send time of last connect/domain list request nodeData->setLastDomainCheckinTimestamp(nodeConnection.lastPingTimestamp); nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting _pendingAssignedNodes.erase(it); NodePermissions userPerms; userPerms.isAssignment = true; userPerms.permissions |= NodePermissions::Permission::canConnectToDomain; // always allow assignment clients to create and destroy entities userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; userPerms.permissions |= NodePermissions::Permission::canWriteToAssetServer; userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent; userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData; userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities; userPerms.permissions |= NodePermissions::Permission::canViewAssetURLs; newNode->setPermissions(userPerms); return newNode; } const QString AUTHENTICATION_ENABLE_OAUTH2 = "authentication.enable_oauth2"; const QString AUTHENTICATION_OAUTH2_URL_PATH = "authentication.oauth2_url_path"; const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; const QString AUTHENTICATION_PLUGIN_CLIENT_ID = "authentication.plugin_client_id"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, const QString& domainAccessToken, const QString& domainRefreshToken) { auto limitedNodeList = DependencyManager::get<LimitedNodeList>(); // start with empty permissions NodePermissions userPerms(NodePermissionsKey(username, 0)); userPerms.setAll(false); // check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress(); bool isLocalUser = (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost); QString verifiedUsername; // if this remains empty, consider this an anonymous connection attempt if (!username.isEmpty()) { const QUuid& connectionToken = _connectionTokenHash.value(username.toLower()); if (usernameSignature.isEmpty() || connectionToken.isNull()) { // user is attempting to prove their identity to us, but we don't have enough information sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); // ask for their public key right now to make sure we have it requestUserPublicKey(username, true); getGroupMemberships(username); // optimistically get started on group memberships #ifdef WANT_DEBUG qDebug() << "stalling login because we have no username-signature:" << username; #endif if (!domainHasLogin() || domainUsername.isEmpty()) { return SharedNodePointer(); } } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they sent us a username and the signature verifies it getGroupMemberships(username); verifiedUsername = username.toLower(); } else { // they sent us a username, but it didn't check out requestUserPublicKey(username); #ifdef WANT_DEBUG qDebug() << "stalling login because signature verification failed:" << username; #endif if (!domainHasLogin() || domainUsername.isEmpty()) { return SharedNodePointer(); } } } // The domain may have its own users and groups. QString verifiedDomainUsername; QStringList verifiedDomainUserGroups; if (domainHasLogin() && !domainUsername.isEmpty()) { if (domainAccessToken.isEmpty()) { // User is attempting to prove their domain identity. #ifdef WANT_DEBUG qDebug() << "Stalling login because we have no domain OAuth2 tokens:" << domainUsername;