diff --git a/BUILD_OSX.md b/BUILD_OSX.md index c8f19710ca..44f27d3d02 100644 --- a/BUILD_OSX.md +++ b/BUILD_OSX.md @@ -3,9 +3,10 @@ Please read the [general build guide](BUILD.md) for information on dependencies ###Homebrew [Homebrew](http://brew.sh/) is an excellent package manager for OS X. It makes install of all High Fidelity dependencies very simple. - brew install cmake openssl qt5 + brew tap homebrew/versions + brew install cmake openssl qt55 -We no longer require install of qt5 via our [homebrew formulas repository](https://github.com/highfidelity/homebrew-formulas). Versions of Qt that are 5.5.x and above provide a mechanism to disable the wireless scanning we previously had a custom patch for. +We no longer require install of qt5 via our [homebrew formulas repository](https://github.com/highfidelity/homebrew-formulas). Versions of Qt that are 5.5.x provide a mechanism to disable the wireless scanning we previously had a custom patch for. ###OpenSSL and Qt diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp index 322fe6e57e..8ba253d549 100644 --- a/assignment-client/src/AssignmentClientMonitor.cpp +++ b/assignment-client/src/AssignmentClientMonitor.cpp @@ -286,8 +286,8 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer()->addOrUpdateNode - (senderID, NodeType::Unassigned, senderSockAddr, senderSockAddr, false, false); + matchingNode = DependencyManager::get()->addOrUpdateNode(senderID, NodeType::Unassigned, + senderSockAddr, senderSockAddr); auto childData = std::unique_ptr { new AssignmentClientChildData(Assignment::Type::AllTypes) }; diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 1fb0674e7d..7f43b86328 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -235,7 +235,7 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN } void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { - if (senderNode->getCanRez()) { + if (senderNode->getCanWriteToAssetServer()) { QString assetPath = message.readString(); auto assetHash = message.read(SHA256_HASH_LENGTH).toHex(); @@ -251,7 +251,7 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode } void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { - if (senderNode->getCanRez()) { + if (senderNode->getCanWriteToAssetServer()) { int numberOfDeletedMappings { 0 }; message.readPrimitive(&numberOfDeletedMappings); @@ -272,7 +272,7 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared } void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) { - if (senderNode->getCanRez()) { + if (senderNode->getCanWriteToAssetServer()) { QString oldPath = message.readString(); QString newPath = message.readString(); @@ -337,7 +337,7 @@ void AssetServer::handleAssetGet(QSharedPointer message, Shared void AssetServer::handleAssetUpload(QSharedPointer message, SharedNodePointer senderNode) { - if (senderNode->getCanRez()) { + if (senderNode->getCanWriteToAssetServer()) { qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID()); auto task = new UploadAssetTask(message, senderNode, _filesDirectory); diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 0555f95c65..7594d5dd2c 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -268,6 +268,14 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio qDebug("wantTerseEditLogging=%s", debug::valueOf(wantTerseEditLogging)); EntityTreePointer tree = std::static_pointer_cast(_tree); + + int maxTmpEntityLifetime; + if (readOptionInt("maxTmpLifetime", settingsSectionObject, maxTmpEntityLifetime)) { + tree->setEntityMaxTmpLifetime(maxTmpEntityLifetime); + } else { + tree->setEntityMaxTmpLifetime(EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME); + } + tree->setWantEditLogging(wantEditLogging); tree->setWantTerseEditLogging(wantTerseEditLogging); } diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index ba00392cd7..bad24dd3a1 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.3, + "version": 1.4, "settings": [ { "name": "metaverse", @@ -56,6 +56,7 @@ "label": "Paths", "help": "Clients can enter a path to reach an exact viewpoint in your domain.
Add rows to the table below to map a path to a viewpoint.
The index path ( / ) is where clients will enter if they do not enter an explicit path.", "type": "table", + "can_add_new_rows": true, "key": { "name": "path", "label": "Path", @@ -157,27 +158,6 @@ "help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.", "value-hidden": true }, - { - "name": "restricted_access", - "type": "checkbox", - "label": "Restricted Access", - "default": false, - "help": "Only users listed in \"Allowed Users\" can enter your domain." - }, - { - "name": "allowed_users", - "type": "table", - "label": "Allowed Users", - "help": "You can always connect from the domain-server machine.", - "numbered": false, - "columns": [ - { - "name": "username", - "label": "Username", - "can_set": true - } - ] - }, { "name": "maximum_user_capacity", "label": "Maximum User Capacity", @@ -187,25 +167,141 @@ "advanced": false }, { - "name": "allowed_editors", + "name": "standard_permissions", "type": "table", - "label": "Allowed Editors", - "help": "List the High Fidelity names for people you want to be able lock or unlock entities in this domain.
An empty list means everyone.", - "numbered": false, + "label": "Domain-Wide User Permissions", + "help": "Indicate which users or groups can have which domain-wide permissions.", + "caption": "Standard Permissions", + "can_add_new_rows": false, + + "groups": [ + { + "label": "User / Group", + "span": 1 + }, + { + "label": "Permissions ?", + "span": 6 + } + ], + "columns": [ { - "name": "username", - "label": "Username", - "can_set": true + "name": "permissions_id", + "label": "" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": true + }, + { + "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 } - ] + ], + + "non-deletable-row-key": "permissions_id", + "non-deletable-row-values": ["localhost", "anonymous", "logged-in"] }, { - "name": "editors_are_rezzers", - "type": "checkbox", - "label": "Only Editors Can Create Entities", - "help": "Only users listed in \"Allowed Editors\" can create new entites.", - "default": false + "name": "permissions", + "type": "table", + "caption": "Permissions for Specific Users", + "can_add_new_rows": true, + + "groups": [ + { + "label": "User / Group", + "span": 1 + }, + { + "label": "Permissions ?", + "span": 6 + } + ], + + "columns": [ + { + "name": "permissions_id", + "label": "" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": true + }, + { + "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 + } + ] } ] }, @@ -218,6 +314,8 @@ "type": "table", "label": "Persistent Scripts", "help": "Add the URLs for scripts that you would like to ensure are always running in your domain.", + "can_add_new_rows": true, + "columns": [ { "name": "url", @@ -302,6 +400,8 @@ "label": "Zones", "help": "In this table you can define a set of zones in which you can specify various audio properties.", "numbered": false, + "can_add_new_rows": true, + "key": { "name": "name", "label": "Name", @@ -353,6 +453,8 @@ "help": "In this table you can set custom attenuation coefficients between audio zones", "numbered": true, "can_order": true, + "can_add_new_rows": true, + "columns": [ { "name": "source", @@ -380,6 +482,8 @@ "label": "Reverb Settings", "help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.", "numbered": true, + "can_add_new_rows": true, + "columns": [ { "name": "zone", @@ -479,6 +583,14 @@ "label": "Entity Server Settings", "assignment-types": [6], "settings": [ + { + "name": "maxTmpLifetime", + "label": "Maximum Lifetime of Temporary Entities", + "help": "The maximum number of seconds for the lifetime of an entity which will be considered \"temporary\".", + "placeholder": "3600", + "default": "3600", + "advanced": true + }, { "name": "persistFilePath", "label": "Entities File Path", @@ -501,6 +613,8 @@ "label": "Backup Rules", "help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.", "numbered": false, + "can_add_new_rows": true, + "default": [ {"Name":"Half Hourly Rolling","backupInterval":1800,"format":".backup.halfhourly.%N","maxBackupVersions":5}, {"Name":"Daily Rolling","backupInterval":86400,"format":".backup.daily.%N","maxBackupVersions":7}, diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index efb9e907c5..2862feed87 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -20,6 +20,17 @@ body { top: 40px; } +.table .value-row td, .table .inputs td { + vertical-align: middle; +} + +.table .table-checkbox { + /* Fix IE sizing checkboxes to fill table cell */ + width: auto; + margin-left: auto; + margin-right: auto; +} + .glyphicon-remove { font-size: 24px; } @@ -107,6 +118,58 @@ table { word-wrap: break-word; } +caption { + color: #333; + font-weight: 700; + padding-top: 0; +} + +table > tbody > .headers > td { + vertical-align: middle; +} + +table .headers + .headers td { + font-size: 13px; + color: #222; +} + +table[name="security.standard_permissions"] .headers td + td, table[name="security.permissions"] .headers td + td { + text-align: center; +} + +.tooltip.top .tooltip-arrow { + border-top-color: #fff; + border-width: 10px 10px 0; + margin-bottom: -5px; +} + +.tooltip-inner { + padding: 20px 20px 10px 20px; + font-size: 14px; + text-align: left; + color: #333; + background-color: #fff; + box-shadow: 0 3px 8px 8px #e8e8e8; +} + +.tooltip.in { + opacity: 1; +} + +.tooltip-inner ul { + padding-left: 0; + margin-bottom: 15px; +} + +.tooltip-inner li { + list-style-type: none; + margin-bottom: 5px; +} + +#security .tooltip-inner { + max-width: 520px; +} + #xs-advanced-container { margin-bottom: 20px; } diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index e17a886e10..aecc48b31f 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -232,6 +232,17 @@ $(document).ready(function(){ badgeSidebarForDifferences($(this)); }); + // Bootstrap switch in table + $('#' + Settings.FORM_ID).on('change', 'input.table-checkbox', function () { + // Bootstrap switches in table: set the changed data attribute for all rows in table. + var row = $(this).closest('tr'); + if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table. + row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true); + updateDataChangedForSiblingRows(row, true); + badgeSidebarForDifferences($(this)); + } + }); + $('.advanced-toggle').click(function(){ Settings.showAdvanced = !Settings.showAdvanced var advancedSelector = $('.' + Settings.ADVANCED_CLASS) @@ -841,6 +852,8 @@ function reloadSettings(callback) { // setup any bootstrap switches $('.toggle-checkbox').bootstrapSwitch(); + $('[data-toggle="tooltip"]').tooltip(); + // add tooltip to locked settings $('label.locked').tooltip({ placement: 'right', @@ -875,6 +888,7 @@ function saveSettings() { } } + console.log("----- SAVING ------"); console.log(formJSON); // re-enable all inputs @@ -908,10 +922,33 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" + setting.help + "" } + var nonDeletableRowKey = setting["non-deletable-row-key"]; + var nonDeletableRowValues = setting["non-deletable-row-values"]; + html += ""; + if (setting.caption) { + html += "" + } + + // Column groups + if (setting.groups) { + html += "" + _.each(setting.groups, function (group) { + html += "" + }) + if (!isLocked && !setting.read_only) { + if (setting.can_order) { + html += ""; + } + html += "" + } + html += "" + } + // Column names html += "" @@ -950,6 +987,8 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" } + var isNonDeletableRow = false; + _.each(setting.columns, function(col) { if (isArray) { @@ -961,16 +1000,19 @@ function makeTable(setting, keypath, setting_value, isLocked) { colName = keypath + "." + rowIndexOrName + "." + col.name; } - // setup the td for this column - html += ""; + } else { + // Use a hidden input so that the values are posted. + html += ""; + } - // for values to be posted properly we add a hidden input to this td - html += ""; - - html += ""; }) if (!isLocked && !setting.read_only) { @@ -979,8 +1021,12 @@ function makeTable(setting, keypath, setting_value, isLocked) { "'>" + "" } - html += "" + if (isNonDeletableRow) { + html += ""; + } else { + html += ""; + } } html += "" @@ -990,7 +1036,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { } // populate inputs in the table for new values - if (!isLocked && !setting.read_only) { + if (!isLocked && !setting.read_only && setting.can_add_new_rows) { html += makeTableInputs(setting) } html += "
" + setting.caption + "
" + group.label + "
" + rowIndexOrName + ""; + isNonDeletableRow = isNonDeletableRow + || (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1); - // add the actual value to the td so it is displayed - html += colValue; + if (isArray && col.type === "checkbox" && col.editable) { + html += "" + + "" + + colValue + "
" @@ -1012,17 +1058,23 @@ function makeTableInputs(setting) { } _.each(setting.columns, function(col) { - html += "\ - \ - " + if (col.type === "checkbox") { + html += "" + + ""; + } else { + html += "\ + \ + " + } }) if (setting.can_order) { html += "" } - html += "" + html += "" html += "" return html @@ -1127,11 +1179,11 @@ function addTableRow(add_glyphicon) { } else { $(element).html(1) } - } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) { - $(element).html("") - } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { + } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) { + $(element).html("") + } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { // Change buttons var anchor = $(element).children("a") anchor.removeClass(Settings.ADD_ROW_SPAN_CLASSES) @@ -1142,8 +1194,20 @@ function addTableRow(add_glyphicon) { input.remove() } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) { // Hide inputs - var input = $(element).children("input") - input.attr("type", "hidden") + var input = $(element).find("input") + var isCheckbox = false; + if (input.hasClass("table-checkbox")) { + input = $(input).parent(); + isCheckbox = true; + } + + var val = input.val(); + if (isCheckbox) { + val = $(input).find("input").is(':checked'); + // don't hide the checkbox + } else { + input.attr("type", "hidden") + } if (isArray) { var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length @@ -1152,14 +1216,22 @@ function addTableRow(add_glyphicon) { // are there multiple columns or just one? // with multiple we have an array of Objects, with one we have an array of whatever the value type is var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length - input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + + if (isCheckbox) { + $(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + } else { + input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "")) + } } else { input.attr("name", full_name + "." + $(element).attr("name")) } - input.attr("data-changed", "true") - - $(element).append(input.val()) + if (isCheckbox) { + $(input).find("input").attr("data-changed", "true"); + } else { + input.attr("data-changed", "true"); + $(element).append(val); + } } else { console.log("Unknown table element") } diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index b940d46849..c4a7d1a425 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -26,7 +26,7 @@ using SharedAssignmentPointer = QSharedPointer; DomainGatekeeper::DomainGatekeeper(DomainServer* server) : _server(server) { - + } void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID, @@ -38,7 +38,7 @@ void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID) { auto it = _pendingAssignedNodes.find(tempUUID); - + if (it != _pendingAssignedNodes.end()) { return it->second.getAssignmentUUID(); } else { @@ -62,59 +62,56 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSenderSockAddr(), - DomainHandler::ConnectionRefusedReason::ProtocolMismatch); + 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 }; - + 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; - + if (pendingAssignment != _pendingAssignedNodes.end()) { node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QString username; QByteArray usernameSignature; - + if (message->getBytesLeftToRead() > 0) { // read username from packet packetStream >> username; - + if (message->getBytesLeftToRead() > 0) { // read user signature from packet packetStream >> usernameSignature; } } - + node = processAgentConnectRequest(nodeConnection, username, usernameSignature); } - + if (node) { // set the sending sock addr and node interest set on this node DomainServerNodeData* nodeData = reinterpret_cast(node->getLinkedData()); nodeData->setSendingSockAddr(message->getSenderSockAddr()); nodeData->setNodeInterestSet(nodeConnection.interestList.toSet()); nodeData->setPlaceName(nodeConnection.placeName); - + // signal that we just connected a node so the DomainServer can get it a list // and broadcast its presence right away emit connectedNode(node); @@ -123,18 +120,72 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer nodesToKill; + + auto limitedNodeList = DependencyManager::get(); + limitedNodeList->eachNode([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){ + QString username = node->getPermissions().getUserName(); + NodePermissions userPerms(username); + + if (node->getPermissions().isAssignment) { + // this node is an assignment-client + userPerms.isAssignment = true; + userPerms.canAdjustLocks = true; + userPerms.canRezPermanentEntities = true; + userPerms.canRezTemporaryEntities = true; + } else { + // this node is an agent + userPerms.setAll(false); + + const QHostAddress& addr = node->getLocalSocket().getAddress(); + bool isLocalUser = (addr == limitedNodeList->getLocalSockAddr().getAddress() || + addr == QHostAddress::LocalHost); + if (isLocalUser) { + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost); + } + + if (username.isEmpty()) { + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous); + } else { + if (_server->_settingsManager.havePermissionsForName(username)) { + userPerms = _server->_settingsManager.getPermissionsForName(username); + } else { + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn); + } + } + } + + node->setPermissions(userPerms); + + if (!userPerms.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" @@ -149,124 +200,99 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo qDebug() << "No assignment was deployed with UUID" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID); return SharedNodePointer(); } - + // add the new node SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection); - + DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); - + // set assignment related data on the linked data for this node nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID()); nodeData->setWalletUUID(it->second.getWalletUUID()); nodeData->setNodeVersion(it->second.getNodeVersion()); nodeData->setWasAssigned(true); - + // cleanup the PendingAssignedNodeData for this assignment now that it's connecting _pendingAssignedNodes.erase(it); - + // always allow assignment clients to create and destroy entities - newNode->setIsAllowedEditor(true); - newNode->setCanRez(true); - + NodePermissions userPerms; + userPerms.isAssignment = true; + userPerms.canAdjustLocks = true; + userPerms.canRezPermanentEntities = true; + userPerms.canRezTemporaryEntities = true; + newNode->setPermissions(userPerms); return newNode; } const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; -const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors"; -const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers"; SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, const QByteArray& usernameSignature) { - + auto limitedNodeList = DependencyManager::get(); - - bool isRestrictingAccess = - _server->_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); - - // check if this user is on our local machine - if this is true they are always allowed to connect + + // start with empty permissions + NodePermissions userPerms(username); + 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); - - // if we're using restricted access and this user is not local make sure we got a user signature - if (isRestrictingAccess && !isLocalUser) { - if (!username.isEmpty()) { - if (usernameSignature.isEmpty()) { - // if user didn't include usernameSignature in connect request, send a connectionToken packet - sendConnectionTokenPacket(username, nodeConnection.senderSockAddr); - - // ask for their public key right now to make sure we have it - requestUserPublicKey(username); - - return SharedNodePointer(); - } - } + if (isLocalUser) { + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost); + qDebug() << "user-permissions: is local user, so:" << userPerms; } - - bool verifiedUsername = false; - - // if we do not have a local user we need to subject them to our verification and capacity checks - if (!isLocalUser) { - - // check if we need to look at the username signature - if (isRestrictingAccess) { - if (isVerifiedAllowedUser(username, usernameSignature, nodeConnection.senderSockAddr)) { - // we verified the user via their username and signature - set the verifiedUsername - // so we don't re-decrypt their sig if we're trying to exempt them from max capacity check (due to - // being in the allowed editors list) - verifiedUsername = true; - } else { - // failed to verify user - return a null shared ptr - return SharedNodePointer(); - } - } - - if (!isWithinMaxCapacity(username, usernameSignature, verifiedUsername, nodeConnection.senderSockAddr)) { - // we can't allow this user to connect because we are at max capacity (and they either aren't an allowed editor - // or couldn't be verified as one) + + if (!username.isEmpty() && usernameSignature.isEmpty()) { + // 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); + if (!userPerms.canConnectToDomain) { return SharedNodePointer(); } } - - // if this user is in the editors list (or if the editors list is empty) set the user's node's isAllowedEditor to true - const QVariant* allowedEditorsVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); - QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); - - // if the allowed editors list is empty then everyone can adjust locks - bool isAllowedEditor = allowedEditors.empty(); - - if (allowedEditors.contains(username, Qt::CaseInsensitive)) { - // we have a non-empty allowed editors list - check if this user is verified to be in it - if (!verifiedUsername) { - if (!verifyUserSignature(username, usernameSignature, HifiSockAddr())) { - // failed to verify a user that is in the allowed editors list - - // TODO: fix public key refresh in interface/metaverse and force this check - qDebug() << "Could not verify user" << username << "as allowed editor. In the interim this user" - << "will be given edit rights to avoid a thrasing of public key requests and connect requests."; - } - - isAllowedEditor = true; + + if (username.isEmpty()) { + // they didn't tell us who they are + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous); + qDebug() << "user-permissions: no username, so:" << userPerms; + } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { + // they are sent us a username and the signature verifies it + if (_server->_settingsManager.havePermissionsForName(username)) { + // we have specific permissions for this user. + userPerms = _server->_settingsManager.getPermissionsForName(username); + qDebug() << "user-permissions: specific user matches, so:" << userPerms; } else { - // already verified this user and they are in the allowed editors list - isAllowedEditor = true; + // they are logged into metaverse, but we don't have specific permissions for them. + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn); + qDebug() << "user-permissions: user is logged in, so:" << userPerms; + } + userPerms.setUserName(username); + } else { + // they sent us a username, but it didn't check out + requestUserPublicKey(username); + if (!userPerms.canConnectToDomain) { + return SharedNodePointer(); } } - - // check if only editors should be able to rez entities - const QVariant* editorsAreRezzersVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH); - - bool onlyEditorsAreRezzers = false; - if (editorsAreRezzersVariant) { - onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool(); + + qDebug() << "user-permissions: final:" << userPerms; + + if (!userPerms.canConnectToDomain) { + sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", + nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers); + return SharedNodePointer(); } - - bool canRez = true; - if (onlyEditorsAreRezzers) { - canRez = isAllowedEditor; + + if (!userPerms.canConnectPastMaxCapacity && !isWithinMaxCapacity()) { + // we can't allow this user to connect because we are at max capacity + sendConnectionDeniedPacket("Too many connected users.", nodeConnection.senderSockAddr, + DomainHandler::ConnectionRefusedReason::TooManyUsers); + return SharedNodePointer(); } QUuid hintNodeID; @@ -285,24 +311,23 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect return true; }); - + // add the connecting node (or re-use the matched one from eachNodeBreakable above) SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID); - + // set the edit rights for this user - newNode->setIsAllowedEditor(isAllowedEditor); - newNode->setCanRez(canRez); + newNode->setPermissions(userPerms); // grab the linked data for our new node so we can set the username DomainServerNodeData* nodeData = reinterpret_cast(newNode->getLinkedData()); - + // if we have a username from the connect request, set it on the DomainServerNodeData nodeData->setUsername(username); - + // 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); - + return newNode; } @@ -310,11 +335,11 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node QUuid nodeID) { HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr; SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID); - + if (connectedPeer) { // this user negotiated a connection with us via ICE, so re-use their ICE client ID nodeID = nodeConnection.connectUUID; - + if (connectedPeer->getActiveSocket()) { // set their discovered socket to whatever the activated socket on the network peer object was discoveredSocket = *connectedPeer->getActiveSocket(); @@ -325,15 +350,15 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node nodeID = QUuid::createUuid(); } } - + auto limitedNodeList = DependencyManager::get(); - + SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeID, nodeConnection.nodeType, nodeConnection.publicSockAddr, nodeConnection.localSockAddr); - + // So that we can send messages to this node at will - we need to activate the correct socket on this node now newNode->activateMatchingOrNewSymmetricSocket(discoveredSocket); - + return newNode; } @@ -343,21 +368,21 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, // it's possible this user can be allowed to connect, but we need to check their username signature QByteArray publicKeyArray = _userPublicKeys.value(username); - + const QUuid& connectionToken = _connectionTokenHash.value(username.toLower()); - + if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) { // if we do have a public key for the user, check for a signature match - + const unsigned char* publicKeyData = reinterpret_cast(publicKeyArray.constData()); - + // first load up the public key into an RSA struct RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size()); - + QByteArray lowercaseUsername = username.toLower().toUtf8(); QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()), QCryptographicHash::Sha256); - + if (rsaPublicKey) { int decryptResult = RSA_verify(NID_sha256, reinterpret_cast(usernameWithToken.constData()), @@ -365,29 +390,29 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, reinterpret_cast(usernameSignature.constData()), usernameSignature.size(), rsaPublicKey); - + if (decryptResult == 1) { - qDebug() << "Username signature matches for" << username << "- allowing connection."; - + qDebug() << "Username signature matches for" << username; + // free up the public key and remove connection token before we return RSA_free(rsaPublicKey); _connectionTokenHash.remove(username); - + return true; - + } else { if (!senderSockAddr.isNull()) { qDebug() << "Error decrypting username signature for " << username << "- denying connection."; sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr, DomainHandler::ConnectionRefusedReason::LoginError); } - + // free up the public key, we don't need it anymore RSA_free(rsaPublicKey); } - + } else { - + // we can't let this user in since we couldn't convert their public key to an RSA key we could use if (!senderSockAddr.isNull()) { qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection."; @@ -402,86 +427,35 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, DomainHandler::ConnectionRefusedReason::LoginError); } } - + requestUserPublicKey(username); // no joy. maybe next time? return false; } -bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature, - const HifiSockAddr& senderSockAddr) { - - if (username.isEmpty()) { - qDebug() << "Connect request denied - no username provided."; - - sendConnectionDeniedPacket("No username provided", senderSockAddr, - DomainHandler::ConnectionRefusedReason::LoginError); - - return false; - } - - QStringList allowedUsers = - _server->_settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList(); - - if (allowedUsers.contains(username, Qt::CaseInsensitive)) { - if (!verifyUserSignature(username, usernameSignature, senderSockAddr)) { - return false; - } - } else { - qDebug() << "Connect request denied for user" << username << "- not in allowed users list."; - sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr, - DomainHandler::ConnectionRefusedReason::NotAuthorized); - - return false; - } - - return true; -} - -bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature, - bool& verifiedUsername, - const HifiSockAddr& senderSockAddr) { +bool DomainGatekeeper::isWithinMaxCapacity() { // find out what our maximum capacity is - const QVariant* maximumUserCapacityVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); + const QVariant* maximumUserCapacityVariant = + valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; if (maximumUserCapacity > 0) { unsigned int connectedUsers = _server->countConnectedUsers(); if (connectedUsers >= maximumUserCapacity) { - // too many users, deny the new connection unless this user is an allowed editor - - const QVariant* allowedEditorsVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH); - - QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList(); - if (allowedEditors.contains(username)) { - if (verifiedUsername || verifyUserSignature(username, usernameSignature, senderSockAddr)) { - verifiedUsername = true; - qDebug() << "Above maximum capacity -" << connectedUsers << "/" << maximumUserCapacity << - "but user" << username << "is in allowed editors list so will be allowed to connect."; - return true; - } - } - - // deny connection from this user qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection."; - sendConnectionDeniedPacket("Too many connected users.", senderSockAddr, - DomainHandler::ConnectionRefusedReason::TooManyUsers); - return false; } - + qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, allowing new connection."; } - + return true; } void DomainGatekeeper::preloadAllowedUserPublicKeys() { - const QVariant* allowedUsersVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH); - QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList(); - + QStringList allowedUsers = _server->_settingsManager.getAllNames(); + if (allowedUsers.size() > 0) { // in the future we may need to limit how many requests here - for now assume that lists of allowed users are not // going to create > 100 requests @@ -492,15 +466,20 @@ void DomainGatekeeper::preloadAllowedUserPublicKeys() { } void DomainGatekeeper::requestUserPublicKey(const QString& username) { + // don't request public keys for the standard psuedo-account-names + if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) { + return; + } + // even if we have a public key for them right now, request a new one in case it has just changed JSONCallbackParameters callbackParams; callbackParams.jsonCallbackReceiver = this; callbackParams.jsonCallbackMethod = "publicKeyJSONCallback"; - + const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key"; - + qDebug() << "Requesting public key for user" << username; - + DependencyManager::get()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username), AccountManagerAuth::None, QNetworkAccessManager::GetOperation, callbackParams); @@ -508,38 +487,47 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) { void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) { QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); - + if (jsonObject["status"].toString() == "success") { // figure out which user this is for - + const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key"; QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING); - + if (usernameRegex.indexIn(requestReply.url().toString()) != -1) { QString username = usernameRegex.cap(1); - + qDebug() << "Storing a public key for user" << username; - + // pull the public key as a QByteArray from this response const QString JSON_DATA_KEY = "data"; const QString JSON_PUBLIC_KEY_KEY = "public_key"; - + _userPublicKeys[username] = QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()); } } } -void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, - DomainHandler::ConnectionRefusedReason reasonCode) { +void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr) { + QString protocolVersionError = "Protocol version mismatch - Domain version: " + QCoreApplication::applicationVersion(); + + qDebug() << "Protocol Version mismatch - denying connection."; + + sendConnectionDeniedPacket(protocolVersionError, senderSockAddr, + DomainHandler::ConnectionRefusedReason::ProtocolMismatch); +} + +void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, + DomainHandler::ConnectionRefusedReason reasonCode) { // this is an agent and we've decided we won't let them connect - send them a packet to deny connection QByteArray utfString = reason.toUtf8(); quint16 payloadSize = utfString.size(); - + // setup the DomainConnectionDenied packet - auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, - payloadSize + sizeof(payloadSize) + sizeof(uint8_t)); - + auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, + payloadSize + sizeof(payloadSize) + sizeof(uint8_t)); + // pack in the reason the connection was denied (the client displays this) if (payloadSize > 0) { uint8_t reasonCodeWire = (uint8_t)reasonCode; @@ -547,7 +535,7 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H connectionDeniedPacket->writePrimitive(payloadSize); connectionDeniedPacket->write(utfString); } - + // send the packet off DependencyManager::get()->sendPacket(std::move(connectionDeniedPacket), senderSockAddr); } @@ -555,20 +543,20 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr) { // get the existing connection token or create a new one QUuid& connectionToken = _connectionTokenHash[username.toLower()]; - + if (connectionToken.isNull()) { connectionToken = QUuid::createUuid(); } - + // setup a static connection token packet static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID); - + // reset the packet before each time we send connectionTokenPacket->reset(); - + // write the connection token connectionTokenPacket->write(connectionToken.toRfc4122()); - + // send off the packet unreliably DependencyManager::get()->sendUnreliablePacket(*connectionTokenPacket, senderSockAddr); } @@ -576,33 +564,33 @@ void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const const int NUM_PEER_PINGS_BEFORE_DELETE = 2000 / UDP_PUNCH_PING_INTERVAL_MS; void DomainGatekeeper::pingPunchForConnectingPeer(const SharedNetworkPeer& peer) { - + if (peer->getConnectionAttempts() >= NUM_PEER_PINGS_BEFORE_DELETE) { // we've reached the maximum number of ping attempts qDebug() << "Maximum number of ping attempts reached for peer with ID" << peer->getUUID(); qDebug() << "Removing from list of connecting peers."; - + _icePeers.remove(peer->getUUID()); } else { auto limitedNodeList = DependencyManager::get(); - + // send the ping packet to the local and public sockets for this node auto localPingPacket = limitedNodeList->constructICEPingPacket(PingType::Local, limitedNodeList->getSessionUUID()); limitedNodeList->sendPacket(std::move(localPingPacket), peer->getLocalSocket()); - + auto publicPingPacket = limitedNodeList->constructICEPingPacket(PingType::Public, limitedNodeList->getSessionUUID()); limitedNodeList->sendPacket(std::move(publicPingPacket), peer->getPublicSocket()); - + peer->incrementConnectionAttempts(); } } void DomainGatekeeper::handlePeerPingTimeout() { NetworkPeer* senderPeer = qobject_cast(sender()); - + if (senderPeer) { SharedNetworkPeer sharedPeer = _icePeers.value(senderPeer->getUUID()); - + if (sharedPeer && !sharedPeer->getActiveSocket()) { pingPunchForConnectingPeer(sharedPeer); } @@ -613,24 +601,24 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointergetMessage()); - + NetworkPeer* receivedPeer = new NetworkPeer; iceResponseStream >> *receivedPeer; - + if (!_icePeers.contains(receivedPeer->getUUID())) { qDebug() << "New peer requesting ICE connection being added to hash -" << *receivedPeer; SharedNetworkPeer newPeer = SharedNetworkPeer(receivedPeer); _icePeers[receivedPeer->getUUID()] = newPeer; - + // make sure we know when we should ping this peer connect(newPeer.data(), &NetworkPeer::pingTimerTimeout, this, &DomainGatekeeper::handlePeerPingTimeout); - + // immediately ping the new peer, and start a timer to continue pinging it until we connect to it newPeer->startPingTimer(); - + qDebug() << "Sending ping packets to establish connectivity with ICE peer with ID" << newPeer->getUUID(); - + pingPunchForConnectingPeer(newPeer); } else { delete receivedPeer; @@ -640,18 +628,18 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer message) { auto limitedNodeList = DependencyManager::get(); auto pingReplyPacket = limitedNodeList->constructICEPingReplyPacket(*message, limitedNodeList->getSessionUUID()); - + limitedNodeList->sendPacket(std::move(pingReplyPacket), message->getSenderSockAddr()); } void DomainGatekeeper::processICEPingReplyPacket(QSharedPointer message) { QDataStream packetStream(message->getMessage()); - + QUuid nodeUUID; packetStream >> nodeUUID; - + SharedNetworkPeer sendingPeer = _icePeers.value(nodeUUID); - + if (sendingPeer) { // we had this NetworkPeer in our connecting list - add the right sock addr to our connected list sendingPeer->activateMatchingOrNewSymmetricSocket(message->getSenderSockAddr()); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 09e3b04ed7..50bbf38543 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -42,6 +42,8 @@ public: void preloadAllowedUserPublicKeys(); void removeICEPeer(const QUuid& peerUUID) { _icePeers.remove(peerUUID); } + + static void sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr); public slots: void processConnectRequestPacket(QSharedPointer message); void processICEPingPacket(QSharedPointer message); @@ -51,8 +53,12 @@ public slots: void publicKeyJSONCallback(QNetworkReply& requestReply); signals: + void killNode(SharedNodePointer node); void connectedNode(SharedNodePointer node); - + +public slots: + void updateNodePermissions(); + private slots: void handlePeerPingTimeout(); private: @@ -66,18 +72,14 @@ private: bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); - bool isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature, - const HifiSockAddr& senderSockAddr); - bool isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature, - bool& verifiedUsername, - const HifiSockAddr& senderSockAddr); + bool isWithinMaxCapacity(); bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); void sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr); - void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, - DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown); + static void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, + DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown); void pingPunchForConnectingPeer(const SharedNetworkPeer& peer); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 0f5498a575..7c596bb187 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -101,6 +101,13 @@ DomainServer::DomainServer(int argc, char* argv[]) : // make sure we hear about newly connected nodes from our gatekeeper connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode); + // if a connected node loses connection privileges, hang up on it + connect(&_gatekeeper, &DomainGatekeeper::killNode, this, &DomainServer::handleKillNode); + + // if permissions are updated, relay the changes to the Node datastructures + connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions, + &_gatekeeper, &DomainGatekeeper::updateNodePermissions); + if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) { // we either read a certificate and private key or were not passed one // and completed login or did not need to @@ -318,16 +325,11 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) { auto nodeList = DependencyManager::get(); - // This implements a special case that handles OLD clients which don't know how to negotiate matching - // protocol versions. We know these clients will sent DomainConnectRequest with older versions. We also - // know these clients will show a warning dialog if they get an EntityData with a protocol version they - // don't understand, so we can send them an empty EntityData with our latest version and they will - // warn the user that the protocol is not compatible - if (headerType == PacketType::DomainConnectRequest && - headerVersion < static_cast(DomainConnectRequestVersion::HasProtocolVersions)) { - auto packetWithBadVersion = NLPacket::create(PacketType::EntityData); - nodeList->sendPacket(std::move(packetWithBadVersion), packet.getSenderSockAddr()); - return false; + // if this is a mismatching connect packet, we can't simply drop it on the floor + // send back a packet to the interface that tells them we refuse connection for a mismatch + if (headerType == PacketType::DomainConnectRequest + && headerVersion != versionForPacketType(PacketType::DomainConnectRequest)) { + DomainGatekeeper::sendProtocolMismatchConnectionDenial(packet.getSenderSockAddr()); } // let the normal nodeList implementation handle all other packets. @@ -800,8 +802,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif extendedHeaderStream << limitedNodeList->getSessionUUID(); extendedHeaderStream << node->getUUID(); - extendedHeaderStream << (quint8) node->isAllowedEditor(); - extendedHeaderStream << (quint8) node->getCanRez(); + extendedHeaderStream << node->getPermissions(); auto domainListPackets = NLPacketList::create(PacketType::DomainList, extendedHeader); @@ -1093,11 +1094,12 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; - // Add a flag to indicate if this domain uses restricted access - - // for now that will exclude it from listings - static const QString RESTRICTED_ACCESS_FLAG = "restricted"; - domainObject[RESTRICTED_ACCESS_FLAG] = - _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool(); + // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings + const QString RESTRICTED_ACCESS_FLAG = "restricted"; + + // consider the domain to have restricted access if "anonymous" connections can't connect to the domain. + NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); + domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain; // Add the metadata to the heartbeat static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat"; @@ -2107,35 +2109,42 @@ void DomainServer::processPathQueryPacket(QSharedPointer messag void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer message) { // This packet has been matched to a source node and they're asking not to be in the domain anymore auto limitedNodeList = DependencyManager::get(); - + const QUuid& nodeUUID = message->getSourceID(); - + qDebug() << "Received a disconnect request from node with UUID" << nodeUUID; - + // we want to check what type this node was before going to kill it so that we can avoid sending the RemovedNode // packet to nodes that don't care about this type auto nodeToKill = limitedNodeList->nodeWithUUID(nodeUUID); - + if (nodeToKill) { - auto nodeType = nodeToKill->getType(); - limitedNodeList->killNodeWithUUID(nodeUUID); - - static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID); - - removedNodePacket->reset(); - removedNodePacket->write(nodeUUID.toRfc4122()); - - // broadcast out the DomainServerRemovedNode message - limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool { - // only send the removed node packet to nodes that care about the type of node this was - auto nodeLinkedData = dynamic_cast(otherNode->getLinkedData()); - return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType); - }, [&limitedNodeList](const SharedNodePointer& otherNode){ - limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode); - }); + handleKillNode(nodeToKill); } } +void DomainServer::handleKillNode(SharedNodePointer nodeToKill) { + auto nodeType = nodeToKill->getType(); + auto limitedNodeList = DependencyManager::get(); + const QUuid& nodeUUID = nodeToKill->getUUID(); + + limitedNodeList->killNodeWithUUID(nodeUUID); + + static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID); + + removedNodePacket->reset(); + removedNodePacket->write(nodeUUID.toRfc4122()); + + // broadcast out the DomainServerRemovedNode message + limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool { + // only send the removed node packet to nodes that care about the type of node this was + auto nodeLinkedData = dynamic_cast(otherNode->getLinkedData()); + return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType); + }, [&limitedNodeList](const SharedNodePointer& otherNode){ + limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode); + }); +} + void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer message) { static const int NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN = 3; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 8b8409ff0a..bdcc36c1ac 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -114,6 +114,8 @@ private: unsigned int countConnectedUsers(); + void handleKillNode(SharedNodePointer nodeToKill); + void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr); QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 0ca0cf8232..5790eb9178 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include + #include #include #include @@ -26,6 +28,9 @@ #include "DomainServerSettingsManager.h" +#define WANT_DEBUG 1 + + const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; const QString DESCRIPTION_SETTINGS_KEY = "settings"; @@ -44,7 +49,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() : QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH); descriptionFile.open(QIODevice::ReadOnly); - QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll()); + QJsonParseError parseError; + QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll(), &parseError); if (descriptionDocument.isObject()) { QJsonObject descriptionObject = descriptionDocument.object(); @@ -63,8 +69,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() : } static const QString MISSING_SETTINGS_DESC_MSG = - QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.") - .arg(SETTINGS_DESCRIPTION_RELATIVE_PATH); + QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3") + .arg(SETTINGS_DESCRIPTION_RELATIVE_PATH).arg(parseError.errorString()).arg(parseError.offset); static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6; QMetaObject::invokeMethod(QCoreApplication::instance(), "queuedQuit", Qt::QueuedConnection, @@ -88,7 +94,8 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointersetAll(true); + _standardAgentPermissions[NodePermissions::standardNameAnonymous].reset( + new NodePermissions(NodePermissions::standardNameAnonymous)); + _standardAgentPermissions[NodePermissions::standardNameLoggedIn].reset( + new NodePermissions(NodePermissions::standardNameLoggedIn)); + + if (isRestrictedAccess) { + // only users in allow-users list can connect + _standardAgentPermissions[NodePermissions::standardNameAnonymous]->canConnectToDomain = false; + _standardAgentPermissions[NodePermissions::standardNameLoggedIn]->canConnectToDomain = false; + } // else anonymous and logged-in retain default of canConnectToDomain = true + + foreach (QString allowedUser, allowedUsers) { + // even if isRestrictedAccess is false, we have to add explicit rows for these users. + // defaults to canConnectToDomain = true + _agentPermissions[allowedUser].reset(new NodePermissions(allowedUser)); + } + + foreach (QString allowedEditor, allowedEditors) { + if (!_agentPermissions.contains(allowedEditor)) { + _agentPermissions[allowedEditor].reset(new NodePermissions(allowedEditor)); + if (isRestrictedAccess) { + // they can change locks, but can't connect. + _agentPermissions[allowedEditor]->canConnectToDomain = false; + } + } + _agentPermissions[allowedEditor]->canAdjustLocks = true; + } + + QList> permissionsSets; + permissionsSets << _standardAgentPermissions << _agentPermissions; + foreach (auto permissionsSet, permissionsSets) { + foreach (QString userName, permissionsSet.keys()) { + if (onlyEditorsAreRezzers) { + permissionsSet[userName]->canRezPermanentEntities = permissionsSet[userName]->canAdjustLocks; + permissionsSet[userName]->canRezTemporaryEntities = permissionsSet[userName]->canAdjustLocks; + } else { + permissionsSet[userName]->canRezPermanentEntities = true; + permissionsSet[userName]->canRezTemporaryEntities = true; + } + } + } + + packPermissions(); + _standardAgentPermissions.clear(); + _agentPermissions.clear(); + } } + unpackPermissions(); + // write the current description version to our settings appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion); } +void DomainServerSettingsManager::packPermissionsForMap(QString mapName, + QHash agentPermissions, + QString keyPath) { + QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security"); + if (!security || !security->canConvert(QMetaType::QVariantMap)) { + security = valueForKeyPath(_configMap.getUserConfig(), "security", true); + (*security) = QVariantMap(); + } + + // save settings for anonymous / logged-in / localhost + QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath); + if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) { + permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath, true); + (*permissions) = QVariantList(); + } + + QVariantList* permissionsList = reinterpret_cast(permissions); + (*permissionsList).clear(); + foreach (QString userName, agentPermissions.keys()) { + *permissionsList += agentPermissions[userName]->toVariant(); + } +} + +void DomainServerSettingsManager::packPermissions() { + // transfer details from _agentPermissions to _configMap + packPermissionsForMap("standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH); + + // save settings for specific users + packPermissionsForMap("permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH); + + persistToFile(); + _configMap.loadMasterAndUserConfig(_argumentList); +} + +void DomainServerSettingsManager::unpackPermissions() { + // transfer details from _configMap to _agentPermissions; + + _standardAgentPermissions.clear(); + _agentPermissions.clear(); + + bool foundLocalhost = false; + bool foundAnonymous = false; + bool foundLoggedIn = false; + bool needPack = false; + + QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH); + if (!standardPermissions || !standardPermissions->canConvert(QMetaType::QVariantList)) { + qDebug() << "failed to extract standard permissions from settings."; + standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH, true); + (*standardPermissions) = QVariantList(); + } + QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) { + qDebug() << "failed to extract permissions from settings."; + permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH, true); + (*permissions) = QVariantList(); + } + + QList standardPermissionsList = standardPermissions->toList(); + foreach (QVariant permsHash, standardPermissionsList) { + NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; + QString id = perms->getID(); + foundLocalhost |= (id == NodePermissions::standardNameLocalhost); + foundAnonymous |= (id == NodePermissions::standardNameAnonymous); + foundLoggedIn |= (id == NodePermissions::standardNameLoggedIn); + if (_standardAgentPermissions.contains(id)) { + qDebug() << "duplicate name in standard permissions table: " << id; + _standardAgentPermissions[id] |= perms; + needPack = true; + } else { + _standardAgentPermissions[id] = perms; + } + } + + QList permissionsList = permissions->toList(); + foreach (QVariant permsHash, permissionsList) { + NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; + QString id = perms->getID(); + if (_agentPermissions.contains(id)) { + qDebug() << "duplicate name in permissions table: " << id; + _agentPermissions[id] |= perms; + needPack = true; + } else { + _agentPermissions[id] = perms; + } + } + + // if any of the standard names are missing, add them + if (!foundLocalhost) { + NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLocalhost) }; + perms->setAll(true); + _standardAgentPermissions[perms->getID()] = perms; + needPack = true; + } + if (!foundAnonymous) { + NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameAnonymous) }; + _standardAgentPermissions[perms->getID()] = perms; + needPack = true; + } + if (!foundLoggedIn) { + NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLoggedIn) }; + _standardAgentPermissions[perms->getID()] = perms; + needPack = true; + } + + if (needPack) { + packPermissions(); + } + + #ifdef WANT_DEBUG + qDebug() << "--------------- permissions ---------------------"; + QList> permissionsSets; + permissionsSets << _standardAgentPermissions << _agentPermissions; + foreach (auto permissionSet, permissionsSets) { + QHashIterator i(permissionSet); + while (i.hasNext()) { + i.next(); + NodePermissionsPointer perms = i.value(); + qDebug() << i.key() << perms; + } + } + #endif +} + +NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const QString& name) const { + if (_standardAgentPermissions.contains(name)) { + return *(_standardAgentPermissions[name].get()); + } + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; +} + +NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const { + if (_agentPermissions.contains(name)) { + return *(_agentPermissions[name].get()); + } + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; +} + QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) { const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath); @@ -257,7 +470,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection qDebug() << "DomainServerSettingsManager postedObject -" << postedObject; // we recurse one level deep below each group for the appropriate setting - recurseJSONObjectAndOverwriteSettings(postedObject); + bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject); // store whatever the current _settingsMap is to file persistToFile(); @@ -267,8 +480,13 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); // defer a restart to the domain-server, this gives our HTTPConnection enough time to respond - const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000; - QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); + if (restartRequired) { + const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000; + QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); + } else { + unpackPermissions(); + emit updateNodePermissions(); + } return true; } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) { @@ -282,7 +500,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true); rootObject[SETTINGS_RESPONSE_LOCKED_VALUES_KEY] = QJsonDocument::fromVariant(_configMap.getMasterConfig()).object(); - connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json"); } @@ -458,6 +675,8 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV // TODO: we still need to recurse here with the description in case values in the array have special types settingMap[key] = newValue.toArray().toVariantList(); } + + sortPermissions(); } QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) { @@ -471,9 +690,10 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson return QJsonObject(); } -void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) { +bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) { auto& settingsVariant = _configMap.getUserConfig(); - + bool needRestart = false; + // Iterate on the setting groups foreach(const QString& rootKey, postedObject.keys()) { QJsonValue rootValue = postedObject[rootKey]; @@ -521,6 +741,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ if (!matchingDescriptionObject.isEmpty()) { updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject); + if (rootKey != "security") { + needRestart = true; + } } else { qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting."; } @@ -534,6 +757,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ if (!matchingDescriptionObject.isEmpty()) { QJsonValue settingValue = rootValue.toObject()[settingKey]; updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject); + if (rootKey != "security") { + needRestart = true; + } } else { qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey << "- cannot update setting."; @@ -549,9 +775,42 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ // re-merge the user and master configs after a settings change _configMap.mergeMasterAndUserConfigs(); + + return needRestart; +} + +// Compare two members of a permissions list +bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) { + if (!v1.canConvert(QMetaType::QVariantMap) || + !v2.canConvert(QMetaType::QVariantMap)) { + return v1.toString() < v2.toString(); + } + QVariantMap m1 = v1.toMap(); + QVariantMap m2 = v2.toMap(); + + if (!m1.contains("permissions_id") || + !m2.contains("permissions_id")) { + return v1.toString() < v2.toString(); + } + return m1["permissions_id"].toString() < m2["permissions_id"].toString(); +} + +void DomainServerSettingsManager::sortPermissions() { + // sort the permission-names + QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH); + if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) { + QList* standardPermissionsList = reinterpret_cast(standardPermissions); + std::sort((*standardPermissionsList).begin(), (*standardPermissionsList).end(), permissionVariantLessThan); + } + QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + if (permissions && permissions->canConvert(QMetaType::QVariantList)) { + QList* permissionsList = reinterpret_cast(permissions); + std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan); + } } void DomainServerSettingsManager::persistToFile() { + sortPermissions(); // make sure we have the dir the settings file is supposed to live in QFileInfo settingsFileInfo(_configMap.getUserConfigFilename()); diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index d6dd5070a9..446e9a2eed 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -19,14 +19,14 @@ #include #include +#include "NodePermissions.h" const QString SETTINGS_PATHS_KEY = "paths"; const QString SETTINGS_PATH = "/settings"; const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; - -const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users"; -const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access"; +const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; +const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; class DomainServerSettingsManager : public QObject { Q_OBJECT @@ -41,16 +41,29 @@ public: QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); } QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } + bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name); } + bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); } + NodePermissions getStandardPermissionsForName(const QString& name) const; + NodePermissions getPermissionsForName(const QString& name) const; + QStringList getAllNames() { return _agentPermissions.keys(); } + +signals: + void updateNodePermissions(); + + private slots: void processSettingsRequestPacket(QSharedPointer message); private: + QStringList _argumentList; + QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false); - void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject); + bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject); void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName); + void sortPermissions(); void persistToFile(); double _descriptionVersion; @@ -58,6 +71,12 @@ private: HifiConfigVariantMap _configMap; friend class DomainServer; + + void packPermissionsForMap(QString mapName, QHash agentPermissions, QString keyPath); + void packPermissions(); + void unpackPermissions(); + QHash _standardAgentPermissions; // anonymous, logged-in, localhost + QHash _agentPermissions; // specific account-names }; #endif // hifi_DomainServerSettingsManager_h diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index cd6dc8ede0..df3210a20d 100755 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -199,7 +199,7 @@ FocusScope { anchors.leftMargin: hifi.dimensions.textPadding anchors.verticalCenter: parent.verticalCenter id: popupText - text: listView.model[index] + text: listView.model[index] ? listView.model[index] : "" size: hifi.fontSizes.textFieldInput color: hifi.colors.baseGray } diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index 015a192185..93ccbc0b8c 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -186,7 +186,12 @@ ModalWindow { } if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) { + if (root.selectDirectory) { + currentSelection.text = currentText !== "This PC" ? currentText : ""; + d.currentSelectionUrl = helper.pathToUrl(currentText); + } fileTableModel.folder = folder; + fileTableView.forceActiveFocus(); } } } @@ -212,9 +217,11 @@ ModalWindow { function update() { var row = fileTableView.currentRow; - openButton.text = root.selectDirectory && row === -1 ? "Choose" : "Open" - if (row === -1) { + if (!root.selectDirectory) { + currentSelection.text = ""; + currentSelectionIsFolder = false; + } return; } @@ -445,12 +452,6 @@ ModalWindow { onSortIndicatorOrderChanged: { updateSort(); } - onActiveFocusChanged: { - if (activeFocus && currentRow == -1) { - fileTableView.selection.select(0) - } - } - itemDelegate: Item { clip: true @@ -607,6 +608,12 @@ ModalWindow { readOnly: !root.saveDialog activeFocusOnTab: !readOnly onActiveFocusChanged: if (activeFocus) { selectAll(); } + onTextChanged: { + if (root.saveDialog && text !== "") { + fileTableView.selection.clear(); + fileTableView.currentRow = -1; + } + } onAccepted: okAction.trigger(); } @@ -652,7 +659,7 @@ ModalWindow { Action { id: okAction - text: root.saveDialog ? "Save" : (root.selectDirectory ? "Choose" : "Open") + text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open" enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false onTriggered: { if (!root.selectDirectory && !d.currentSelectionIsFolder @@ -676,7 +683,6 @@ ModalWindow { return; } - // Handle the ambiguity between different cases // * typed name (with or without extension) // * full path vs relative vs filename only diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 68e916c29e..49c2e17d84 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -630,7 +630,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); - connect(&domainHandler, &DomainHandler::resetting, nodeList.data(), &NodeList::resetDomainServerCheckInVersion); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one @@ -654,9 +653,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID); connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); - connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, this, &Application::limitOfSilentDomainCheckInsReached); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); + // you might think we could just do this in NodeList but we only want this connection for Interface + connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset); + // connect to appropriate slots on AccountManager auto accountManager = DependencyManager::get(); @@ -1074,8 +1075,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) : void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) { switch (static_cast(reasonCode)) { case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: - notifyPacketVersionMismatch(); - break; case DomainHandler::ConnectionRefusedReason::TooManyUsers: case DomainHandler::ConnectionRefusedReason::Unknown: { QString message = "Unable to connect to the location you are visiting.\n"; @@ -4293,7 +4292,7 @@ void Application::nodeActivated(SharedNodePointer node) { if (assetDialog) { auto nodeList = DependencyManager::get(); - if (nodeList->getThisNodeCanRez()) { + if (nodeList->getThisNodeCanWriteAssets()) { // call reload on the shown asset browser dialog to get the mappings (if permissions allow) QMetaObject::invokeMethod(assetDialog, "reload"); } else { @@ -4619,17 +4618,6 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const { Physics::setSessionUUID(sessionUUID); } - -// If we're not getting anything back from the domain server checkin, it might be that the domain speaks an -// older version of the DomainConnectRequest protocol. We will attempt to send and older version of DomainConnectRequest. -// We won't actually complete the connection, but if the server responds, we know that it needs to be upgraded (or we -// need to be downgraded to talk to it). -void Application::limitOfSilentDomainCheckInsReached() { - auto nodeList = DependencyManager::get(); - nodeList->downgradeDomainServerCheckInVersion(); // attempt to use an older domain checkin version - nodeList->reset(); -} - bool Application::askToSetAvatarUrl(const QString& url) { QUrl realUrl(url); if (realUrl.isLocalFile()) { @@ -4800,7 +4788,7 @@ void Application::toggleRunningScriptsWidget() const { } void Application::toggleAssetServerWidget(QString filePath) { - if (!DependencyManager::get()->getThisNodeCanRez()) { + if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { return; } diff --git a/interface/src/Application.h b/interface/src/Application.h index f93434f581..6b6148be32 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -318,7 +318,6 @@ private slots: bool displayAvatarAttachmentConfirmationDialog(const QString& name) const; void setSessionUUID(const QUuid& sessionUUID) const; - void limitOfSilentDomainCheckInsReached(); void domainChanged(const QString& domainHostname); void updateWindowTitle() const; diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 031564fa7a..b37f70f65d 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -136,8 +136,8 @@ Menu::Menu() { Qt::CTRL | Qt::SHIFT | Qt::Key_A, qApp, SLOT(toggleAssetServerWidget())); auto nodeList = DependencyManager::get(); - QObject::connect(nodeList.data(), &NodeList::canRezChanged, assetServerAction, &QAction::setEnabled); - assetServerAction->setEnabled(nodeList->getThisNodeCanRez()); + QObject::connect(nodeList.data(), &NodeList::canWriteAssetsChanged, assetServerAction, &QAction::setEnabled); + assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets()); // Edit > Package Model... [advanced] addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0, @@ -542,6 +542,9 @@ Menu::Menu() { #if (PR_BUILD || DEV_BUILD) addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false, qApp, SLOT(sendWrongProtocolVersionsSignature(bool))); + + addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongDSConnectVersion, 0, false, + nodeList.data(), SLOT(toggleSendNewerDSConnectVersion(bool))); #endif diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 8081e27eb8..503cbf51fa 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -166,6 +166,7 @@ namespace MenuOption { const QString RunTimingTests = "Run Timing Tests"; const QString ScriptEditor = "Script Editor..."; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; + const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; const QString SendWrongProtocolVersion = "Send wrong protocol version"; const QString SetHomeLocation = "Set Home Location"; const QString ShowDSConnectTable = "Show Domain Connection Timing"; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index d09fc60d9b..e0863041a1 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -32,6 +32,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged); connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged); + connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged); } void EntityScriptingInterface::queueEntityMessage(PacketType packetType, @@ -49,6 +50,11 @@ bool EntityScriptingInterface::canRez() { return nodeList->getThisNodeCanRez(); } +bool EntityScriptingInterface::canRezTmp() { + auto nodeList = DependencyManager::get(); + return nodeList->getThisNodeCanRezTmp(); +} + void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) { if (_entityTree) { disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 8ae6a77dab..e9024eb721 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -80,6 +80,7 @@ public slots: // returns true if the DomainServer will allow this Node/Avatar to rez new entities Q_INVOKABLE bool canRez(); + Q_INVOKABLE bool canRezTmp(); /// adds a model with the specific properties Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties, bool clientOnly = false); @@ -179,6 +180,7 @@ signals: void canAdjustLocksChanged(bool canAdjustLocks); void canRezChanged(bool canRez); + void canRezTmpChanged(bool canRez); void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 581e0a9568..77a0c6d6fe 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -26,6 +26,8 @@ #include "LogHandler.h" static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50; +const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour + EntityTree::EntityTree(bool shouldReaverage) : Octree(shouldReaverage), @@ -128,13 +130,16 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI EntityItemProperties properties = origProperties; bool allowLockChange; + bool canRezPermanentEntities; QUuid senderID; if (senderNode.isNull()) { auto nodeList = DependencyManager::get(); allowLockChange = nodeList->isAllowedEditor(); + canRezPermanentEntities = nodeList->getThisNodeCanRez(); senderID = nodeList->getSessionUUID(); } else { allowLockChange = senderNode->isAllowedEditor(); + canRezPermanentEntities = senderNode->getCanRez(); senderID = senderNode->getUUID(); } @@ -143,6 +148,12 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return false; } + if (!canRezPermanentEntities && (entity->getLifetime() != properties.getLifetime())) { + // we don't allow a Node that can't create permanent entities to adjust lifetimes on existing ones + qCDebug(entities) << "Refusing disallowed entity lifetime adjustment."; + return false; + } + // enforce support for locked entities. If an entity is currently locked, then the only // property we allow you to change is the locked property. if (entity->getLocked()) { @@ -308,17 +319,39 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI return true; } +bool EntityTree::permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) { + float lifeTime = properties.getLifetime(); + + if (lifeTime == 0.0f || lifeTime > _maxTmpEntityLifetime) { + // this is an attempt to rez a permanent entity. + if (!canRez) { + return false; + } + } else { + // this is an attempt to rez a temporary entity. + if (!canRezTmp) { + return false; + } + } + + return true; +} + EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer result = NULL; + auto nodeList = DependencyManager::get(); + if (!nodeList) { + qDebug() << "EntityTree::addEntity -- can't get NodeList"; + return nullptr; + } + bool clientOnly = properties.getClientOnly(); - if (!clientOnly && getIsClient()) { + if (!clientOnly && getIsClient() && + !permissionsAllowRez(properties, nodeList->getThisNodeCanRez(), nodeList->getThisNodeCanRezTmp())) { // if our Node isn't allowed to create entities in this domain, don't try. - auto nodeList = DependencyManager::get(); - if (nodeList && !nodeList->getThisNodeCanRez()) { - return NULL; - } + return nullptr; } bool recordCreationTime = false; @@ -920,7 +953,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endUpdate = usecTimestampNow(); _totalUpdates++; } else if (message.getType() == PacketType::EntityAdd) { - if (senderNode->getCanRez()) { + if (permissionsAllowRez(properties, senderNode->getCanRez(), senderNode->getCanRezTmp())) { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); startCreate = usecTimestampNow(); @@ -1430,6 +1463,12 @@ bool EntityTree::readFromMap(QVariantMap& map) { QVariantList entitiesQList = map["Entities"].toList(); QScriptEngine scriptEngine; + if (entitiesQList.length() == 0) { + // Empty map or invalidly formed file. + return false; + } + + bool success = true; foreach (QVariant entityVariant, entitiesQList) { // QVariantMap --> QScriptValue --> EntityItemProperties --> Entity QVariantMap entityMap = entityVariant.toMap(); @@ -1447,9 +1486,10 @@ bool EntityTree::readFromMap(QVariantMap& map) { EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType(); + success = false; } } - return true; + return success; } void EntityTree::resetClientEditStats() { diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index a85624c9ae..8afb8d878f 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -62,6 +62,10 @@ public: void createRootElement(); + + void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; } + bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp); + /// Implements our type specific root element factory virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override; @@ -252,6 +256,8 @@ public: void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID); + static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; + public slots: void callLoader(EntityItemID entityID); @@ -331,6 +337,8 @@ protected: // we maintain a list of avatarIDs to notice when an entity is a child of one. QSet _avatarIDs; // IDs of avatars connected to entity server QHash> _childrenOfAvatars; // which entities are children of which avatars + + float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME }; }; #endif // hifi_EntityTree_h diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index 1b7ed11cce..80989acd2c 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -144,12 +144,21 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // 4. domain network address (IP or dns resolvable hostname) // use our regex'ed helpers to figure out what we're supposed to do with this - if (!handleUsername(lookupUrl.authority())) { + if (handleUsername(lookupUrl.authority())) { + // handled a username for lookup + + // in case we're failing to connect to where we thought this user was + // store their username as previous lookup so we can refresh their location via API + _previousLookup = lookupUrl; + } else { // we're assuming this is either a network address or global place name // check if it is a network address first bool hostChanged; if (handleNetworkAddress(lookupUrl.host() - + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) { + + // a network address lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); // If the host changed then we have already saved to history if (hostChanged) { @@ -165,10 +174,16 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { // we may have a path that defines a relative viewpoint - if so we should jump to that now handlePath(path, trigger); } else if (handleDomainID(lookupUrl.host())){ + // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info + _previousLookup = lookupUrl; + // no place name - this is probably a domain ID // try to look up the domain ID on the metaverse API attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger); } else { + // store this place name as the previous lookup in case we fail to connect and want to refresh API info + _previousLookup = lookupUrl; + // wasn't an address - lookup the place name // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger); @@ -180,9 +195,13 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) { } else if (lookupUrl.toString().startsWith('/')) { qCDebug(networking) << "Going to relative path" << lookupUrl.path(); + // a path lookup clears the previous lookup since we don't expect to re-attempt it + _previousLookup.clear(); + // if this is a relative path then handle it as a relative viewpoint handlePath(lookupUrl.path(), trigger, true); emit lookupResultsFinished(); + return true; } @@ -276,7 +295,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const qCDebug(networking) << "Possible domain change required to connect to" << domainHostname << "on" << domainPort; - emit possibleDomainChangeRequired(domainHostname, domainPort); + emit possibleDomainChangeRequired(domainHostname, domainPort, domainID); } else { QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString(); @@ -315,7 +334,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString(); if (!overridePath.isEmpty()) { - handlePath(overridePath, trigger); + // make sure we don't re-handle an overriden path if this was a refresh of info from API + if (trigger != LookupTrigger::AttemptedRefresh) { + handlePath(overridePath, trigger); + } } else { // take the path that came back const QString PLACE_PATH_KEY = "path"; @@ -362,7 +384,7 @@ void AddressManager::handleAPIError(QNetworkReply& errorReply) { if (errorReply.error() == QNetworkReply::ContentNotFoundError) { // if this is a lookup that has no result, don't keep re-trying it - //_previousLookup.clear(); + _previousLookup.clear(); emit lookupResultIsNotFound(); } @@ -598,7 +620,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress); - emit possibleDomainChangeRequired(hostname, port); + emit possibleDomainChangeRequired(hostname, port, QUuid()); return hostChanged; } @@ -618,6 +640,13 @@ void AddressManager::goToUser(const QString& username) { QByteArray(), nullptr, requestParams); } +void AddressManager::refreshPreviousLookup() { + // if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history) + if (!_previousLookup.isEmpty()) { + handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh); + } +} + void AddressManager::copyAddress() { QApplication::clipboard()->setText(currentAddress().toString()); } @@ -629,7 +658,10 @@ void AddressManager::copyPath() { void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) { // if we're cold starting and this is called for the first address (from settings) we don't do anything - if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) { + if (trigger != LookupTrigger::StartupFromSettings + && trigger != LookupTrigger::DomainPathResponse + && trigger != LookupTrigger::AttemptedRefresh) { + if (trigger == LookupTrigger::Back) { // we're about to push to the forward stack // if it's currently empty emit our signal to say that going forward is now possible diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index 643924ff5c..a3aaee3ba2 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -48,7 +48,8 @@ public: Forward, StartupFromSettings, DomainPathResponse, - Internal + Internal, + AttemptedRefresh }; bool isConnected(); @@ -89,6 +90,8 @@ public slots: void goToUser(const QString& username); + void refreshPreviousLookup(); + void storeCurrentAddress(); void copyAddress(); @@ -99,7 +102,7 @@ signals: void lookupResultIsOffline(); void lookupResultIsNotFound(); - void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort); + void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID); void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID); void locationChangeRequired(const glm::vec3& newPosition, @@ -152,6 +155,8 @@ private: quint64 _lastBackPush = 0; QString _newHostLookupPath; + + QUrl _previousLookup; }; #endif // hifi_AddressManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 4f85296f03..6880b7a329 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -14,6 +14,7 @@ #include #include +#include "AddressManager.h" #include "Assignment.h" #include "HifiSockAddr.h" #include "NodeList.h" @@ -28,17 +29,10 @@ DomainHandler::DomainHandler(QObject* parent) : QObject(parent), - _uuid(), _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)), - _assignmentUUID(), - _connectionToken(), - _iceDomainID(), - _iceClientID(), - _iceServerSockAddr(), _icePeer(this), - _isConnected(false), - _settingsObject(), - _settingsTimer(this) + _settingsTimer(this), + _apiRefreshTimer(this) { _sockAddr.setObjectName("DomainServer"); @@ -49,6 +43,16 @@ DomainHandler::DomainHandler(QObject* parent) : static const int DOMAIN_SETTINGS_TIMEOUT_MS = 5000; _settingsTimer.setInterval(DOMAIN_SETTINGS_TIMEOUT_MS); connect(&_settingsTimer, &QTimer::timeout, this, &DomainHandler::settingsReceiveFail); + + // setup the API refresh timer for auto connection information refresh from API when failing to connect + const int API_REFRESH_TIMEOUT_MSEC = 2500; + _apiRefreshTimer.setInterval(API_REFRESH_TIMEOUT_MSEC); + + auto addressManager = DependencyManager::get(); + connect(&_apiRefreshTimer, &QTimer::timeout, addressManager.data(), &AddressManager::refreshPreviousLookup); + + // stop the refresh timer if we connect to a domain + connect(this, &DomainHandler::connectedToDomain, &_apiRefreshTimer, &QTimer::stop); } void DomainHandler::disconnect() { @@ -93,10 +97,14 @@ void DomainHandler::softReset() { clearSettings(); + _domainConnectionRefusals.clear(); _connectionDenialsSinceKeypairRegen = 0; // cancel the failure timeout for any pending requests for settings QMetaObject::invokeMethod(&_settingsTimer, "stop"); + + // restart the API refresh timer in case we fail to connect and need to refresh information + QMetaObject::invokeMethod(&_apiRefreshTimer, "start"); } void DomainHandler::hardReset() { @@ -105,7 +113,7 @@ void DomainHandler::hardReset() { softReset(); qCDebug(networking) << "Hard reset in NodeList DomainHandler."; - _iceDomainID = QUuid(); + _pendingDomainID = QUuid(); _iceServerSockAddr = HifiSockAddr(); _hostname = QString(); _sockAddr.clear(); @@ -139,7 +147,9 @@ void DomainHandler::setUUID(const QUuid& uuid) { } } -void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { +void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) { + + _pendingDomainID = domainID; if (hostname != _hostname || _sockAddr.getPort() != port) { // re-set the domain info so that auth information is reloaded @@ -149,9 +159,6 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { // set the new hostname _hostname = hostname; - // FIXME - is this the right place??? - _domainConnectionRefusals.clear(); - qCDebug(networking) << "Updated domain hostname to" << _hostname; // re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname @@ -174,14 +181,15 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) { } void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) { - if (id != _uuid) { + + if (_iceServerSockAddr.getAddress().toString() != iceServerHostname || id != _pendingDomainID) { // re-set the domain info to connect to new domain hardReset(); // refresh our ICE client UUID to something new _iceClientID = QUuid::createUuid(); - _iceDomainID = id; + _pendingDomainID = id; HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr; replaceableSockAddr->~HifiSockAddr(); @@ -255,6 +263,7 @@ void DomainHandler::setIsConnected(bool isConnected) { // we've connected to new domain - time to ask it for global settings requestDomainSettings(); + } else { emit disconnectedFromDomain(); } @@ -305,6 +314,9 @@ void DomainHandler::processICEPingReplyPacket(QSharedPointer me qCDebug(networking) << "Received reply from domain-server on" << senderSockAddr; if (getIP().isNull()) { + // we're hearing back from this domain-server, no need to refresh API information + _apiRefreshTimer.stop(); + // for now we're unsafely assuming this came back from the domain if (senderSockAddr == _icePeer.getLocalSocket()) { qCDebug(networking) << "Connecting to domain using local socket"; @@ -333,17 +345,20 @@ void DomainHandler::processDTLSRequirementPacket(QSharedPointer void DomainHandler::processICEResponsePacket(QSharedPointer message) { if (_icePeer.hasSockets()) { qDebug() << "Received an ICE peer packet for domain-server but we already have sockets. Not processing."; - // bail on processing this packet if our ice peer doesn't have sockets + // bail on processing this packet if our ice peer already has sockets return; } + // start or restart the API refresh timer now that we have new information + _apiRefreshTimer.start(); + QDataStream iceResponseStream(message->getMessage()); iceResponseStream >> _icePeer; DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation); - if (_icePeer.getUUID() != _iceDomainID) { + if (_icePeer.getUUID() != _pendingDomainID) { qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection."; _icePeer.reset(); } else { @@ -373,6 +388,9 @@ bool DomainHandler::reasonSuggestsLogin(ConnectionRefusedReason reasonCode) { } void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer message) { + // we're hearing from this domain-server, don't need to refresh API info + _apiRefreshTimer.stop(); + // Read deny reason from packet uint8_t reasonCodeWire; diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index bcee7668d1..1328174e87 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -58,8 +58,8 @@ public: const QUuid& getAssignmentUUID() const { return _assignmentUUID; } void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; } - - const QUuid& getICEDomainID() const { return _iceDomainID; } + + const QUuid& getPendingDomainID() const { return _pendingDomainID; } const QUuid& getICEClientID() const { return _iceClientID; } @@ -75,7 +75,6 @@ public: bool hasSettings() const { return !_settingsObject.isEmpty(); } void requestDomainSettings(); const QJsonObject& getSettingsObject() const { return _settingsObject; } - void setPendingPath(const QString& pendingPath) { _pendingPath = pendingPath; } const QString& getPendingPath() { return _pendingPath; } @@ -94,7 +93,7 @@ public: }; public slots: - void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT); + void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid()); void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id); void processSettingsPacketList(QSharedPointer packetList); @@ -136,11 +135,11 @@ private: HifiSockAddr _sockAddr; QUuid _assignmentUUID; QUuid _connectionToken; - QUuid _iceDomainID; + QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection QUuid _iceClientID; HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; - bool _isConnected; + bool _isConnected { false }; QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; @@ -148,6 +147,8 @@ private: QStringList _domainConnectionRefusals; bool _hasCheckedForAccessToken { false }; int _connectionDenialsSinceKeypairRegen { 0 }; + + QTimer _apiRefreshTimer; }; #endif // hifi_DomainHandler_h diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 9efe51183e..d7a2d47fab 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -52,7 +52,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short _numCollectedPackets(0), _numCollectedBytes(0), _packetStatTimer(), - _thisNodeCanRez(true) + _permissions(NodePermissions()) { static bool firstCall = true; if (firstCall) { @@ -130,17 +130,22 @@ void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) { } } -void LimitedNodeList::setIsAllowedEditor(bool isAllowedEditor) { - if (_isAllowedEditor != isAllowedEditor) { - _isAllowedEditor = isAllowedEditor; - emit isAllowedEditorChanged(isAllowedEditor); - } -} +void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) { + NodePermissions originalPermissions = _permissions; -void LimitedNodeList::setThisNodeCanRez(bool canRez) { - if (_thisNodeCanRez != canRez) { - _thisNodeCanRez = canRez; - emit canRezChanged(canRez); + _permissions = newPermissions; + + if (originalPermissions.canAdjustLocks != newPermissions.canAdjustLocks) { + emit isAllowedEditorChanged(_permissions.canAdjustLocks); + } + if (originalPermissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) { + emit canRezChanged(_permissions.canRezPermanentEntities); + } + if (originalPermissions.canRezTemporaryEntities != newPermissions.canRezTemporaryEntities) { + emit canRezTmpChanged(_permissions.canRezTemporaryEntities); + } + if (originalPermissions.canWriteToAssetServer != newPermissions.canWriteToAssetServer) { + emit canWriteAssetsChanged(_permissions.canWriteToAssetServer); } } @@ -515,7 +520,7 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) { SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - bool isAllowedEditor, bool canRez, + const NodePermissions& permissions, const QUuid& connectionSecret) { NodeHash::const_iterator it = _nodeHash.find(uuid); @@ -524,14 +529,13 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t matchingNode->setPublicSocket(publicSocket); matchingNode->setLocalSocket(localSocket); - matchingNode->setIsAllowedEditor(isAllowedEditor); - matchingNode->setCanRez(canRez); + matchingNode->setPermissions(permissions); matchingNode->setConnectionSecret(connectionSecret); return matchingNode; } else { // we didn't have this node, so add them - Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, isAllowedEditor, canRez, connectionSecret, this); + Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, permissions, connectionSecret, this); if (nodeType == NodeType::AudioMixer) { LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 5a3c10e8c3..483aa0734c 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -104,12 +104,12 @@ public: const QUuid& getSessionUUID() const { return _sessionUUID; } void setSessionUUID(const QUuid& sessionUUID); - bool isAllowedEditor() const { return _isAllowedEditor; } - void setIsAllowedEditor(bool isAllowedEditor); + void setPermissions(const NodePermissions& newPermissions); + bool isAllowedEditor() const { return _permissions.canAdjustLocks; } + bool getThisNodeCanRez() const { return _permissions.canRezPermanentEntities; } + bool getThisNodeCanRezTmp() const { return _permissions.canRezTemporaryEntities; } + bool getThisNodeCanWriteAssets() const { return _permissions.canWriteToAssetServer; } - bool getThisNodeCanRez() const { return _thisNodeCanRez; } - void setThisNodeCanRez(bool canRez); - quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); } QUdpSocket& getDTLSSocket(); @@ -137,7 +137,7 @@ public: SharedNodePointer addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - bool isAllowedEditor = false, bool canRez = false, + const NodePermissions& permissions = DEFAULT_AGENT_PERMISSIONS, const QUuid& connectionSecret = QUuid()); bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; } @@ -254,6 +254,8 @@ signals: void isAllowedEditorChanged(bool isAllowedEditor); void canRezChanged(bool canRez); + void canRezTmpChanged(bool canRezTmp); + void canWriteAssetsChanged(bool canWriteAssets); protected slots: void connectedForLocalSocketTest(); @@ -300,8 +302,7 @@ protected: int _numCollectedBytes; QElapsedTimer _packetStatTimer; - bool _isAllowedEditor { false }; - bool _thisNodeCanRez; + NodePermissions _permissions; QPointer _initialSTUNTimer; diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp index 34a159ae6c..a11dd69753 100644 --- a/libraries/networking/src/NLPacket.cpp +++ b/libraries/networking/src/NLPacket.cpp @@ -184,6 +184,11 @@ void NLPacket::setType(PacketType type) { writeTypeAndVersion(); } +void NLPacket::setVersion(PacketVersion version) { + _version = version; + writeTypeAndVersion(); +} + void NLPacket::readType() { _type = NLPacket::typeInHeader(*this); } diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h index f49f8498a5..33de262dfb 100644 --- a/libraries/networking/src/NLPacket.h +++ b/libraries/networking/src/NLPacket.h @@ -65,6 +65,7 @@ public: void setType(PacketType type); PacketVersion getVersion() const { return _version; } + void setVersion(PacketVersion version); const QUuid& getSourceID() const { return _sourceID; } diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 1e1cec2413..7201b2fd9a 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -16,6 +16,7 @@ #include "Node.h" #include "SharedUtil.h" +#include "NodePermissions.h" #include #include @@ -47,7 +48,7 @@ const QString& NodeType::getNodeTypeName(NodeType_t nodeType) { } Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, - const HifiSockAddr& localSocket, bool isAllowedEditor, bool canRez, const QUuid& connectionSecret, + const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret, QObject* parent) : NetworkPeer(uuid, publicSocket, localSocket, parent), _type(type), @@ -57,8 +58,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, _clockSkewUsec(0), _mutex(), _clockSkewMovingPercentile(30, 0.8f), // moving 80th percentile of 30 samples - _isAllowedEditor(isAllowedEditor), - _canRez(canRez) + _permissions(permissions) { // Update socket's object name setType(_type); @@ -78,15 +78,12 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); } - QDataStream& operator<<(QDataStream& out, const Node& node) { out << node._type; out << node._uuid; out << node._publicSocket; out << node._localSocket; - out << node._isAllowedEditor; - out << node._canRez; - + out << node._permissions; return out; } @@ -95,9 +92,7 @@ QDataStream& operator>>(QDataStream& in, Node& node) { in >> node._uuid; in >> node._publicSocket; in >> node._localSocket; - in >> node._isAllowedEditor; - in >> node._canRez; - + in >> node._permissions; return in; } diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 3927672319..b277ac0083 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -27,13 +27,14 @@ #include "NodeType.h" #include "SimpleMovingAverage.h" #include "MovingPercentile.h" +#include "NodePermissions.h" class Node : public NetworkPeer { Q_OBJECT public: Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket, - bool isAllowedEditor, bool canRez, const QUuid& connectionSecret = QUuid(), + const NodePermissions& permissions, const QUuid& connectionSecret = QUuid(), QObject* parent = 0); bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; } @@ -58,11 +59,12 @@ public: void updateClockSkewUsec(qint64 clockSkewSample); QMutex& getMutex() { return _mutex; } - void setIsAllowedEditor(bool isAllowedEditor) { _isAllowedEditor = isAllowedEditor; } - bool isAllowedEditor() { return _isAllowedEditor; } - - void setCanRez(bool canRez) { _canRez = canRez; } - bool getCanRez() { return _canRez; } + void setPermissions(const NodePermissions& newPermissions) { _permissions = newPermissions; } + NodePermissions getPermissions() const { return _permissions; } + bool isAllowedEditor() const { return _permissions.canAdjustLocks; } + bool getCanRez() const { return _permissions.canRezPermanentEntities; } + bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; } + bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; } friend QDataStream& operator<<(QDataStream& out, const Node& node); friend QDataStream& operator>>(QDataStream& in, Node& node); @@ -81,8 +83,7 @@ private: qint64 _clockSkewUsec; QMutex _mutex; MovingPercentile _clockSkewMovingPercentile; - bool _isAllowedEditor; - bool _canRez; + NodePermissions _permissions; }; Q_DECLARE_METATYPE(Node*) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 16a4083b08..fd1442d639 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // handle domain change signals from AddressManager connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired, - &_domainHandler, &DomainHandler::setHostnameAndPort); + &_domainHandler, &DomainHandler::setSocketAndID); connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID, &_domainHandler, &DomainHandler::setIceServerHostnameAndID); @@ -250,7 +250,6 @@ void NodeList::sendDomainServerCheckIn() { qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in."; handleICEConnectionToDomainServer(); } else if (!_domainHandler.getIP().isNull()) { - bool isUsingDTLS = false; PacketType domainPacketType = !_domainHandler.isConnected() ? PacketType::DomainConnectRequest : PacketType::DomainListRequest; @@ -292,12 +291,18 @@ void NodeList::sendDomainServerCheckIn() { return; } - auto packetVersion = (domainPacketType == PacketType::DomainConnectRequest) ? _domainConnectRequestVersion : 0; - auto domainPacket = NLPacket::create(domainPacketType, -1, false, false, packetVersion); + auto domainPacket = NLPacket::create(domainPacketType); QDataStream packetStream(domainPacket.get()); if (domainPacketType == PacketType::DomainConnectRequest) { + +#if (PR_BUILD || DEV_BUILD) + if (_shouldSendNewerVersion) { + domainPacket->setVersion(versionForPacketType(domainPacketType) + 1); + } +#endif + QUuid connectUUID; if (!_domainHandler.getAssignmentUUID().isNull()) { @@ -315,18 +320,14 @@ void NodeList::sendDomainServerCheckIn() { packetStream << connectUUID; // include the protocol version signature in our connect request - if (_domainConnectRequestVersion >= static_cast(DomainConnectRequestVersion::HasProtocolVersions)) { - QByteArray protocolVersionSig = protocolVersionsSignature(); - packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); - } + QByteArray protocolVersionSig = protocolVersionsSignature(); + packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size()); } // pack our data to send to the domain-server including // the hostname information (so the domain-server can see which place name we came in on) packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList(); - if (_domainConnectRequestVersion >= static_cast(DomainConnectRequestVersion::HasHostname)) { - packetStream << DependencyManager::get()->getPlaceName(); - } + packetStream << DependencyManager::get()->getPlaceName(); if (!_domainHandler.isConnected()) { DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); @@ -341,9 +342,7 @@ void NodeList::sendDomainServerCheckIn() { flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendDSCheckIn); - if (!isUsingDTLS) { - sendPacket(std::move(domainPacket), _domainHandler.getSockAddr()); - } + sendPacket(std::move(domainPacket), _domainHandler.getSockAddr()); if (_numNoReplyDomainCheckIns >= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) { // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS @@ -462,7 +461,7 @@ void NodeList::handleICEConnectionToDomainServer() { LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(), _domainHandler.getICEClientID(), - _domainHandler.getICEDomainID()); + _domainHandler.getPendingDomainID()); } } @@ -475,7 +474,7 @@ void NodeList::pingPunchForDomainServer() { if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) { qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID" - << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID()); + << uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID()); } else { if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) { // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat @@ -527,7 +526,7 @@ void NodeList::processDomainServerList(QSharedPointer message) DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList); QDataStream packetStream(message->getMessage()); - + // grab the domain's ID from the beginning of the packet QUuid domainUUID; packetStream >> domainUUID; @@ -543,14 +542,11 @@ void NodeList::processDomainServerList(QSharedPointer message) packetStream >> newUUID; setSessionUUID(newUUID); - quint8 isAllowedEditor; - packetStream >> isAllowedEditor; - setIsAllowedEditor((bool) isAllowedEditor); + // pull the permissions/right/privileges for this node out of the stream + NodePermissions newPermissions; + packetStream >> newPermissions; + setPermissions(newPermissions); - quint8 thisNodeCanRez; - packetStream >> thisNodeCanRez; - setThisNodeCanRez((bool) thisNodeCanRez); - // pull each node in the packet while (packetStream.device()->pos() < message->getSize()) { parseNodeFromPacketStream(packetStream); @@ -577,10 +573,9 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) { qint8 nodeType; QUuid nodeUUID, connectionUUID; HifiSockAddr nodePublicSocket, nodeLocalSocket; - bool isAllowedEditor; - bool canRez; + NodePermissions permissions; - packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> isAllowedEditor >> canRez; + packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions; // if the public socket address is 0 then it's reachable at the same IP // as the domain server @@ -591,8 +586,7 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) { packetStream >> connectionUUID; SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket, - nodeLocalSocket, isAllowedEditor, canRez, - connectionUUID); + nodeLocalSocket, permissions, connectionUUID); } void NodeList::sendAssignment(Assignment& assignment) { diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index b269554e77..3fbc86c736 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -68,9 +68,6 @@ public: void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } - /// downgrades the DomainConnnectRequest PacketVersion to attempt to probe for older domain servers - void downgradeDomainServerCheckInVersion() { _domainConnectRequestVersion--; } - public slots: void reset(); void sendDomainServerCheckIn(); @@ -88,8 +85,9 @@ public slots: void processICEPingPacket(QSharedPointer message); - void resetDomainServerCheckInVersion() - { _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); } +#if (PR_BUILD || DEV_BUILD) + void toggleSendNewerDSConnectVersion(bool shouldSendNewerVersion) { _shouldSendNewerVersion = shouldSendNewerVersion; } +#endif signals: void limitOfSilentDomainCheckInsReached(); @@ -105,6 +103,7 @@ private slots: void pingPunchForDomainServer(); void sendKeepAlivePings(); + private: NodeList() : LimitedNodeList(0, 0) { assert(false); } // Not implemented, needed for DependencyManager templates compile NodeList(char ownerType, unsigned short socketListenPort = 0, unsigned short dtlsListenPort = 0); @@ -130,7 +129,9 @@ private: bool _isShuttingDown { false }; QTimer _keepAlivePingTimer; - PacketVersion _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); +#if (PR_BUILD || DEV_BUILD) + bool _shouldSendNewerVersion { false }; +#endif }; #endif // hifi_NodeList_h diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp new file mode 100644 index 0000000000..fb74ccdc94 --- /dev/null +++ b/libraries/networking/src/NodePermissions.cpp @@ -0,0 +1,97 @@ +// +// NodePermissions.cpp +// libraries/networking/src/ +// +// Created by Seth Alves on 2016-6-1. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include "NodePermissions.h" + +QString NodePermissions::standardNameLocalhost = QString("localhost"); +QString NodePermissions::standardNameLoggedIn = QString("logged-in"); +QString NodePermissions::standardNameAnonymous = QString("anonymous"); + +QStringList NodePermissions::standardNames = QList() + << NodePermissions::standardNameLocalhost + << NodePermissions::standardNameLoggedIn + << NodePermissions::standardNameAnonymous; + +NodePermissions& NodePermissions::operator|=(const NodePermissions& rhs) { + this->canConnectToDomain |= rhs.canConnectToDomain; + this->canAdjustLocks |= rhs.canAdjustLocks; + this->canRezPermanentEntities |= rhs.canRezPermanentEntities; + this->canRezTemporaryEntities |= rhs.canRezTemporaryEntities; + this->canWriteToAssetServer |= rhs.canWriteToAssetServer; + this->canConnectPastMaxCapacity |= rhs.canConnectPastMaxCapacity; + return *this; +} +NodePermissions& NodePermissions::operator|=(const NodePermissionsPointer& rhs) { + if (rhs) { + *this |= *rhs.get(); + } + return *this; +} +NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs) { + if (lhs && rhs) { + *lhs.get() |= rhs; + } + return lhs; +} + + +QDataStream& operator<<(QDataStream& out, const NodePermissions& perms) { + out << perms.canConnectToDomain; + out << perms.canAdjustLocks; + out << perms.canRezPermanentEntities; + out << perms.canRezTemporaryEntities; + out << perms.canWriteToAssetServer; + out << perms.canConnectPastMaxCapacity; + return out; +} + +QDataStream& operator>>(QDataStream& in, NodePermissions& perms) { + in >> perms.canConnectToDomain; + in >> perms.canAdjustLocks; + in >> perms.canRezPermanentEntities; + in >> perms.canRezTemporaryEntities; + in >> perms.canWriteToAssetServer; + in >> perms.canConnectPastMaxCapacity; + return in; +} + +QDebug operator<<(QDebug debug, const NodePermissions& perms) { + debug.nospace() << "[permissions: " << perms.getID() << " --"; + if (perms.canConnectToDomain) { + debug << " connect"; + } + if (perms.canAdjustLocks) { + debug << " locks"; + } + if (perms.canRezPermanentEntities) { + debug << " rez"; + } + if (perms.canRezTemporaryEntities) { + debug << " rez-tmp"; + } + if (perms.canWriteToAssetServer) { + debug << " asset-server"; + } + if (perms.canConnectPastMaxCapacity) { + debug << " ignore-max-cap"; + } + debug.nospace() << "]"; + return debug.nospace(); +} +QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms) { + if (perms) { + return operator<<(debug, *perms.get()); + } + debug.nospace() << "[permissions: null]"; + return debug.nospace(); +} diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h new file mode 100644 index 0000000000..c153878a7e --- /dev/null +++ b/libraries/networking/src/NodePermissions.h @@ -0,0 +1,97 @@ +// +// NodePermissions.h +// libraries/networking/src/ +// +// Created by Seth Alves on 2016-6-1. +// Copyright 2016 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_NodePermissions_h +#define hifi_NodePermissions_h + +#include +#include +#include +#include +#include + +class NodePermissions; +using NodePermissionsPointer = std::shared_ptr; + +class NodePermissions { +public: + NodePermissions() { _id = QUuid::createUuid().toString(); } + NodePermissions(const QString& name) { _id = name; } + NodePermissions(QMap perms) { + _id = perms["permissions_id"].toString(); + canConnectToDomain = perms["id_can_connect"].toBool(); + canAdjustLocks = perms["id_can_adjust_locks"].toBool(); + canRezPermanentEntities = perms["id_can_rez"].toBool(); + canRezTemporaryEntities = perms["id_can_rez_tmp"].toBool(); + canWriteToAssetServer = perms["id_can_write_to_asset_server"].toBool(); + canConnectPastMaxCapacity = perms["id_can_connect_past_max_capacity"].toBool(); + } + + QString getID() const { return _id; } + + // the _id member isn't authenticated and _username is. + void setUserName(QString userName) { _userName = userName; } + QString getUserName() { return _userName; } + + bool isAssignment { false }; + + // these 3 names have special meaning. + static QString standardNameLocalhost; + static QString standardNameLoggedIn; + static QString standardNameAnonymous; + static QStringList standardNames; + + // the initializations here should match the defaults in describe-settings.json + bool canConnectToDomain { true }; + bool canAdjustLocks { false }; + bool canRezPermanentEntities { false }; + bool canRezTemporaryEntities { false }; + bool canWriteToAssetServer { false }; + bool canConnectPastMaxCapacity { false }; + + void setAll(bool value) { + canConnectToDomain = value; + canAdjustLocks = value; + canRezPermanentEntities = value; + canRezTemporaryEntities = value; + canWriteToAssetServer = value; + canConnectPastMaxCapacity = value; + } + + QVariant toVariant() { + QMap values; + values["permissions_id"] = _id; + values["id_can_connect"] = canConnectToDomain; + values["id_can_adjust_locks"] = canAdjustLocks; + values["id_can_rez"] = canRezPermanentEntities; + values["id_can_rez_tmp"] = canRezTemporaryEntities; + values["id_can_write_to_asset_server"] = canWriteToAssetServer; + values["id_can_connect_past_max_capacity"] = canConnectPastMaxCapacity; + return QVariant(values); + } + + NodePermissions& operator|=(const NodePermissions& rhs); + NodePermissions& operator|=(const NodePermissionsPointer& rhs); + friend QDataStream& operator<<(QDataStream& out, const NodePermissions& perms); + friend QDataStream& operator>>(QDataStream& in, NodePermissions& perms); + +protected: + QString _id; + QString _userName; +}; + +const NodePermissions DEFAULT_AGENT_PERMISSIONS; + +QDebug operator<<(QDebug debug, const NodePermissions& perms); +QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms); +NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs); + +#endif // hifi_NodePermissions_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index db743f81e4..6ca50420f3 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -45,7 +45,7 @@ const QSet RELIABLE_PACKETS = QSet(); PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: - return 18; + return static_cast(DomainListVersion::PermissionsGrid); case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityData: @@ -69,6 +69,9 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::DomainConnectRequest: return static_cast(DomainConnectRequestVersion::HasProtocolVersions); + case PacketType::DomainServerAddedNode: + return static_cast(DomainServerAddedNodeVersion::PermissionsGrid); + default: return 17; } diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 320635379d..ae54450fee 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -199,4 +199,14 @@ enum class DomainConnectionDeniedVersion : PacketVersion { IncludesReasonCode }; +enum class DomainServerAddedNodeVersion : PacketVersion { + PrePermissionsGrid = 17, + PermissionsGrid +}; + +enum class DomainListVersion : PacketVersion { + PrePermissionsGrid = 18, + PermissionsGrid +}; + #endif // hifi_PacketHeaders_h diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 39be760944..475beef03c 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1863,9 +1863,9 @@ bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputSt QJsonDocument asDocument = QJsonDocument::fromJson(jsonBuffer); QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); - readFromMap(asMap); + bool success = readFromMap(asMap); delete[] rawData; - return true; + return success; } void Octree::writeToFile(const char* fileName, OctreeElementPointer element, QString persistAsFileType) { diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index b3920e70bc..5ae5ff740d 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -213,10 +213,12 @@ QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath, bool if (shouldCreateIfMissing || variantMap.contains(firstKey)) { if (dotIndex == -1) { return &variantMap[firstKey]; - } else if (variantMap[firstKey].canConvert(QMetaType::QVariantMap)) { - return valueForKeyPath(*static_cast(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1), - shouldCreateIfMissing); } + if (!variantMap[firstKey].canConvert(QMetaType::QVariantMap)) { + variantMap[firstKey] = QVariantMap(); + } + return valueForKeyPath(*static_cast(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1), + shouldCreateIfMissing); } return NULL; diff --git a/scripts/system/edit.js b/scripts/system/edit.js index afbc679ec4..1232c8d94d 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -1222,7 +1222,7 @@ function handeMenuEvent(menuItem) { Window.alert("No entities have been selected."); } else { var filename = "entities__" + Window.location.hostname + ".svo.json"; - filename = Window.save("Select where to save", filename, "*.json") + filename = Window.save("Select Where to Save", filename, "*.json") if (filename) { var success = Clipboard.exportEntities(filename, selectionManager.selections); if (!success) { @@ -1234,7 +1234,7 @@ function handeMenuEvent(menuItem) { var importURL = null; if (menuItem == "Import Entities") { - var fullPath = Window.browse("Select models to import", "", "*.json"); + var fullPath = Window.browse("Select Model to Import", "", "*.json"); if (fullPath) { importURL = "file:///" + fullPath; }