diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 911732fcef..8cd9136895 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -684,6 +684,79 @@ } ] }, + { + "name": "permissions", + "type": "table", + "caption": "Permissions for Specific Users", + "can_add_new_rows": true, + + "groups": [ + { + "label": "User", + "span": 1 + }, + { + "label": "Permissions ?", + "span": 7 + } + ], + + "columns": [ + { + "name": "permissions_id", + "label": "" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_adjust_locks", + "label": "Lock / Unlock", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez", + "label": "Rez", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_rez_tmp", + "label": "Rez Temporary", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_write_to_asset_server", + "label": "Write Assets", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_connect_past_max_capacity", + "label": "Ignore Max Capacity", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "name": "id_can_kick", + "label": "Kick Users", + "type": "checkbox", + "editable": true, + "default": false + } + ] + }, { "name": "ip_permissions", "type": "table", @@ -757,18 +830,17 @@ ] }, { - "name": "permissions", + "name": "mac_permissions", "type": "table", - "caption": "Permissions for Specific Users", + "caption": "Permissions for Users with MAC Addresses", "can_add_new_rows": true, - "groups": [ { - "label": "User", + "label": "MAC Address", "span": 1 }, { - "label": "Permissions ?", + "label": "Permissions ?", "span": 7 } ], diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 051465efd2..f55a2073d1 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -119,15 +119,20 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointersetNodeInterestSet(safeInterestSet); nodeData->setPlaceName(nodeConnection.placeName); + qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID()) + << "on" << message->getSenderSockAddr() << "with MAC" << nodeConnection.hardwareAddress; + // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away emit connectedNode(node); } else { - qDebug() << "Refusing connection from node at" << message->getSenderSockAddr(); + qDebug() << "Refusing connection from node at" << message->getSenderSockAddr() + << "with hardware address" << nodeConnection.hardwareAddress; } } -NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress) { +NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, + const QHostAddress& senderAddress, const QString& hardwareAddress) { NodePermissions userPerms; userPerms.setAll(false); @@ -144,8 +149,14 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin #ifdef WANT_DEBUG qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms; #endif + if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) { + // this user comes from a MAC we have in our permissions table, apply those permissions + userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress); - if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific MAC 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); @@ -158,6 +169,13 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername); #ifdef WANT_DEBUG qDebug() << "| user-permissions: specific user matches, so:" << userPerms; +#endif + } else if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) { + // this user comes from a MAC we have in our permissions table, apply those permissions + userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress); + +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific MAC 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 @@ -255,7 +273,14 @@ void DomainGatekeeper::updateNodePermissions() { // or the public socket if we haven't activated a socket for the node yet HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket(); - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress()); + QString hardwareAddress; + + DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); + if (nodeData) { + hardwareAddress = nodeData->getHardwareAddress(); + } + + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress); } node->setPermissions(userPerms); @@ -308,6 +333,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(it->second.getWalletUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); + nodeData->setHardwareAddress(nodeConnection.hardwareAddress); nodeData->setWasAssigned(true); // cleanup the PendingAssignedNodeData for this assignment now that it's connecting @@ -369,7 +395,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect } } - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress()); + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress(), + nodeConnection.hardwareAddress); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", @@ -425,6 +452,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // if we have a username from the connect request, set it on the DomainServerNodeData nodeData->setUsername(username); + // set the hardware address passed in the connect request + nodeData->setHardwareAddress(nodeConnection.hardwareAddress); + // also add an interpolation to DomainServerNodeData so that servers can get username in stats nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(newNode->getUUID()), username); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index b7d2a03af6..b17d0f61a4 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -107,7 +107,8 @@ private: QSet _domainOwnerFriends; // keep track of friends of the domain owner QSet _inFlightGroupMembershipsRequests; // keep track of which we've already asked for - NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress); + NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, + const QHostAddress& senderAddress, const QString& hardwareAddress); void getGroupMemberships(const QString& username); // void getIsGroupMember(const QString& username, const QUuid groupID); diff --git a/domain-server/src/DomainServerNodeData.h b/domain-server/src/DomainServerNodeData.h index f95403c779..ff637844f1 100644 --- a/domain-server/src/DomainServerNodeData.h +++ b/domain-server/src/DomainServerNodeData.h @@ -53,6 +53,9 @@ public: void setNodeVersion(const QString& nodeVersion) { _nodeVersion = nodeVersion; } const QString& getNodeVersion() { return _nodeVersion; } + + void setHardwareAddress(const QString& hardwareAddress) { _hardwareAddress = hardwareAddress; } + const QString& getHardwareAddress() { return _hardwareAddress; } void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue); void removeOverrideForKey(const QString& key, const QString& value); @@ -81,6 +84,7 @@ private: bool _isAuthenticated = true; NodeSet _nodeInterestSet; QString _nodeVersion; + QString _hardwareAddress; QString _placeName; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index fbc5fd4bd5..21214ed5f6 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -29,6 +29,8 @@ #include #include +#include "DomainServerNodeData.h" + #include "DomainServerSettingsManager.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; @@ -439,6 +441,9 @@ void DomainServerSettingsManager::packPermissions() { // save settings for IP addresses packPermissionsForMap("permissions", _ipPermissions, IP_PERMISSIONS_KEYPATH); + // save settings for MAC addresses + packPermissionsForMap("permissions", _macPermissions, MAC_PERMISSIONS_KEYPATH); + // save settings for groups packPermissionsForMap("permissions", _groupPermissions, GROUP_PERMISSIONS_KEYPATH); @@ -506,6 +511,17 @@ void DomainServerSettingsManager::unpackPermissions() { } }); + needPack |= unpackPermissionsForKeypath(MAC_PERMISSIONS_KEYPATH, &_macPermissions, + [&](NodePermissionsPointer perms){ + // make sure that this permission row is for a non-empty hardware + if (perms->getKey().first.isEmpty()) { + _macPermissions.remove(perms->getKey()); + + // we removed a row from the MAC permissions, we'll need a re-pack + needPack = true; + } + }); + needPack |= unpackPermissionsForKeypath(GROUP_PERMISSIONS_KEYPATH, &_groupPermissions, [&](NodePermissionsPointer perms){ @@ -558,7 +574,8 @@ void DomainServerSettingsManager::unpackPermissions() { qDebug() << "--------------- permissions ---------------------"; QList> permissionsSets; permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get() - << _groupPermissions.get() << _groupForbiddens.get() << _ipPermissions.get(); + << _groupPermissions.get() << _groupForbiddens.get() + << _ipPermissions.get() << _macPermissions.get(); foreach (auto permissionSet, permissionsSets) { QHashIterator i(permissionSet); while (i.hasNext()) { @@ -653,19 +670,25 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetPermissions().getVerifiedUserName(); - bool hadExistingPermissions = false; + bool newPermissions = false; if (!verifiedUsername.isEmpty()) { // if we have a verified user name for this user, we apply the kick to the username // check if there were already permissions - hadExistingPermissions = havePermissionsForName(verifiedUsername); + bool hadPermissions = havePermissionsForName(verifiedUsername); // grab or create permissions for the given username - destinationPermissions = _agentPermissions[matchingNode->getPermissions().getKey()]; + auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()]; + + newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain); + + // ensure that the connect permission is clear + userPermissions->clear(NodePermissions::Permission::canConnectToDomain); } else { - // otherwise we apply the kick to the IP from active socket for this node - // (falling back to the public socket if not yet active) + // otherwise we apply the kick to the IP from active socket for this node and the MAC address + + // remove connect permissions for the IP (falling back to the public socket if not yet active) auto& kickAddress = matchingNode->getActiveSocket() ? matchingNode->getActiveSocket()->getAddress() : matchingNode->getPublicSocket().getAddress(); @@ -673,32 +696,41 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointercan(NodePermissions::Permission::canConnectToDomain)) { + newPermissions = true; + + ipPermissions->clear(NodePermissions::Permission::canConnectToDomain); + } + + // potentially remove connect permissions for the MAC address + DomainServerNodeData* nodeData = reinterpret_cast(matchingNode->getLinkedData()); + if (nodeData) { + NodePermissionsKey macAddressKey(nodeData->getHardwareAddress(), 0); + + bool hadMACPermissions = hasPermissionsForMAC(nodeData->getHardwareAddress()); + + auto macPermissions = _macPermissions[macAddressKey]; + + if (!hadMACPermissions || macPermissions->can(NodePermissions::Permission::canConnectToDomain)) { + newPermissions = true; + + macPermissions->clear(NodePermissions::Permission::canConnectToDomain); + } + } } - // make sure we didn't already have existing permissions that disallowed connect - if (!hadExistingPermissions - || destinationPermissions->can(NodePermissions::Permission::canConnectToDomain)) { - + if (newPermissions) { qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) - << "after kick request"; - - // ensure that the connect permission is clear - destinationPermissions->clear(NodePermissions::Permission::canConnectToDomain); + << "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()); // we've changed permissions, time to store them to disk and emit our signal to say they have changed packPermissions(); - - emit updateNodePermissions(); } else { - qWarning() << "Received kick request for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) - << "that already did not have permission to connect"; - - // in this case, though we don't expect the node to be connected to the domain, it is - // emit updateNodePermissions so that the DomainGatekeeper kicks it out emit updateNodePermissions(); } @@ -753,6 +785,16 @@ NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddr return nullPermissions; } +NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const { + NodePermissionsKey macKey = NodePermissionsKey(macAddress, 0); + if (_macPermissions.contains(macKey)) { + return *(_macPermissions[macKey].get()); + } + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; +} + NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QString& groupName, QUuid rankID) const { NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID); if (_groupPermissions.contains(groupRankKey)) { diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index c067377ffc..fcc3e9d91d 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -28,6 +28,7 @@ const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions"; +const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions"; const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions"; const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens"; @@ -62,6 +63,10 @@ public: bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); } NodePermissions getPermissionsForIP(const QHostAddress& address) const; + // these give access to permissions for specific MACs from the domain-server settings page + bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, 0); } + NodePermissions getPermissionsForMAC(const QString& macAddress) const; + // these give access to permissions for specific groups from the domain-server settings page bool havePermissionsForGroup(const QString& groupName, QUuid rankID) const { return _groupPermissions.contains(groupName, rankID); @@ -142,6 +147,7 @@ private: NodePermissionsMap _agentPermissions; // specific account-names NodePermissionsMap _ipPermissions; // permissions granted by node IP address + NodePermissionsMap _macPermissions; // permissions granted by node MAC address NodePermissionsMap _groupPermissions; // permissions granted by membership to specific groups NodePermissionsMap _groupForbiddens; // permissions denied due to membership in a specific group diff --git a/domain-server/src/NodeConnectionData.cpp b/domain-server/src/NodeConnectionData.cpp index 13bb9123d8..93d6802d84 100644 --- a/domain-server/src/NodeConnectionData.cpp +++ b/domain-server/src/NodeConnectionData.cpp @@ -29,6 +29,9 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c // NOTE: QDataStream::readBytes() - The buffer is allocated using new []. Destroy it with the delete [] operator. delete[] rawBytes; + + // read the hardware address sent by the client + dataStream >> newHeader.hardwareAddress; } dataStream >> newHeader.nodeType diff --git a/domain-server/src/NodeConnectionData.h b/domain-server/src/NodeConnectionData.h index 9264db637e..bcbbdf0a40 100644 --- a/domain-server/src/NodeConnectionData.h +++ b/domain-server/src/NodeConnectionData.h @@ -28,6 +28,7 @@ public: HifiSockAddr senderSockAddr; QList interestList; QString placeName; + QString hardwareAddress; QByteArray protocolVersion; }; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 7a778edaad..361070b306 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -346,6 +347,28 @@ void NodeList::sendDomainServerCheckIn() { // include the protocol version signature in our connect request QByteArray protocolVersionSig = protocolVersionsSignature(); packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); + + // if possible, include the MAC address for the current interface in our connect request + QString hardwareAddress; + + for (auto networkInterface : QNetworkInterface::allInterfaces()) { + for (auto interfaceAddress : networkInterface.addressEntries()) { + if (interfaceAddress.ip() == _localSockAddr.getAddress()) { + // this is the interface whose local IP matches what we've detected the current IP to be + hardwareAddress = networkInterface.hardwareAddress(); + + // stop checking interfaces and addresses + break; + } + } + + // stop looping if this was the current interface + if (!hardwareAddress.isEmpty()) { + break; + } + } + + packetStream << hardwareAddress; } // pack our data to send to the domain-server including diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 88285602e1..b2fca69b03 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -67,7 +67,7 @@ PacketVersion versionForPacketType(PacketType packetType) { return static_cast(DomainConnectionDeniedVersion::IncludesExtraInfo); case PacketType::DomainConnectRequest: - return static_cast(DomainConnectRequestVersion::HasProtocolVersions); + return static_cast(DomainConnectRequestVersion::HasMACAddress); case PacketType::DomainServerAddedNode: return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 502ecc3951..8d63b972cc 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -207,7 +207,8 @@ enum class AvatarMixerPacketVersion : PacketVersion { enum class DomainConnectRequestVersion : PacketVersion { NoHostname = 17, HasHostname, - HasProtocolVersions + HasProtocolVersions, + HasMACAddress }; enum class DomainConnectionDeniedVersion : PacketVersion {