1284 lines · 58.3 KB · C++
RawDownload
////  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.