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 ec913b27f5..342b8213d1 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 { @@ -65,53 +65,53 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetSenderSockAddr()); 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); @@ -120,18 +120,74 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer nodesToKill; + + auto limitedNodeList = DependencyManager::get(); + limitedNodeList->eachNodeBreakable([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; + } + + return true; + }); + + 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" @@ -146,124 +202,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 + userPerms.setUserName(username); + 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; + } + } 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; @@ -282,24 +313,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; } @@ -307,11 +337,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(); @@ -322,15 +352,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; } @@ -340,21 +370,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()), @@ -362,29 +392,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."; @@ -399,86 +429,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 @@ -489,15 +468,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); @@ -505,22 +489,22 @@ 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()); } @@ -536,16 +520,16 @@ void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr& DomainHandler::ConnectionRefusedReason::ProtocolMismatch); } -void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, +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, + 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; @@ -553,7 +537,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); } @@ -561,20 +545,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); } @@ -582,33 +566,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); } @@ -619,24 +603,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; @@ -646,18 +630,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 237f8d3185..50bbf38543 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -53,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: @@ -68,11 +72,7 @@ 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); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7c20817353..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 @@ -795,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); @@ -1088,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"; @@ -2102,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/src/Application.cpp b/interface/src/Application.cpp index 504d573678..49c2e17d84 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4292,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 { @@ -4788,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/Menu.cpp b/interface/src/Menu.cpp index 4fd5569ebd..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, 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..5892ac0e54 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(); 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/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/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 8375cd0b1e..11b5f5469a 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -526,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; @@ -542,14 +542,9 @@ 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 + packetStream >> _permissions; - quint8 thisNodeCanRez; - packetStream >> thisNodeCanRez; - setThisNodeCanRez((bool) thisNodeCanRez); - // pull each node in the packet while (packetStream.device()->pos() < message->getSize()) { parseNodeFromPacketStream(packetStream); @@ -576,10 +571,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 @@ -590,8 +584,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/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/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;