diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 427dc62520..83dd633d22 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -306,7 +306,37 @@ } ], "non-deletable-row-key": "permissions_id", - "non-deletable-row-values": [ "localhost", "anonymous", "logged-in" ] + "non-deletable-row-values": [ "localhost", "anonymous", "logged-in" ], + "default": [ + { + "id_can_connect": true, + "id_can_rez_tmp_certified": true, + "permissions_id": "anonymous" + }, + { + "id_can_connect": true, + "id_can_rez_tmp_certified": true, + "permissions_id": "friends" + }, + { + "id_can_adjust_locks": true, + "id_can_connect": true, + "id_can_connect_past_max_capacity": true, + "id_can_kick": true, + "id_can_replace_content": true, + "id_can_rez": true, + "id_can_rez_certified": true, + "id_can_rez_tmp": true, + "id_can_rez_tmp_certified": true, + "id_can_write_to_asset_server": true, + "permissions_id": "localhost" + }, + { + "id_can_connect": true, + "id_can_rez_tmp_certified": true, + "permissions_id": "logged-in" + } + ] }, { "name": "group_permissions", diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 1e5b6ac131..525b989259 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -2,10 +2,19 @@ $(document).ready(function(){ var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button'; var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file'; + var UPLOAD_CONTENT_ALLOWED_DIV_ID = 'upload-content-allowed'; + var UPLOAD_CONTENT_RECOVERING_DIV_ID = 'upload-content-recovering'; + + function progressBarHTML(extraClass, label) { + var html = "
"; + html += "
"; + html += label + "
"; + return html; + } function setupBackupUpload() { // construct the HTML needed for the settings backup panel - var html = "
"; + var html = "
"; html += "Upload a content archive (.zip) or entity file (.json, .json.gz) to replace the content of this domain."; html += "
Note: Your domain content will be replaced by the content you upload, but the existing backup files of your domain's content will not immediately be changed.
"; @@ -13,7 +22,10 @@ $(document).ready(function(){ html += ""; html += ""; - html += "
"; + html += "
"; + html += "Restore in progress"; + html += progressBarHTML('recovery', 'Restoring'); + html += "
"; $('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html); } @@ -66,25 +78,30 @@ $(document).ready(function(){ }); var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; + var CONTENT_ARCHIVES_NORMAL_ID = 'content-archives-success'; + var CONTENT_ARCHIVES_ERROR_ID = 'content-archives-error'; var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table'; var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody'; var MANUAL_ARCHIVES_TABLE_ID = 'manual-archives-table'; var MANUAL_ARCHIVES_TBODY_ID = 'manual-archives-tbody'; var AUTO_ARCHIVES_SETTINGS_LINK_ID = 'auto-archives-settings-link'; + var ACTION_MENU_CLASS = 'action-menu'; var automaticBackups = []; var manualBackups = []; function setupContentArchives() { // construct the HTML needed for the content archives panel - var html = "
"; + var html = "
"; html += ""; html += "Your domain server makes regular archives of the content in your domain. In the list below, you can see and download all of your domain content and settings backups. " - html += "Click here to manage automatic content archive intervals."; + html += "Click here to manage automatic content archive intervals."; html += "
"; html += ""; - var backups_table_head = ""; + var backups_table_head = "" + + "" + + ""; html += backups_table_head; html += "
Archive NameArchive DateActions
Archive NameArchive DateActions
"; @@ -95,7 +112,11 @@ $(document).ready(function(){ html += "
"; html += ""; html += backups_table_head; - html += "
"; + html += "
"; + + html += ""; // put the base HTML in the content archives panel $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html(html); @@ -104,8 +125,9 @@ $(document).ready(function(){ var BACKUP_RESTORE_LINK_CLASS = 'restore-backup'; var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup'; var BACKUP_DELETE_LINK_CLASS = 'delete-backup'; + var ACTIVE_BACKUP_ROW_CLASS = 'active-backup'; - function reloadLatestBackups() { + function reloadBackupInformation() { // make a GET request to get backup information to populate the table $.ajax({ url: '/api/backups', @@ -123,41 +145,92 @@ $(document).ready(function(){ // populate the backups tables with the backups function createBackupTableRow(backup) { return "" - + "" + backup.name + "" + + "" + backup.name + "" + moment(backup.createdAtMillis).format('lll') - + "" + + "" + ""; + + "
  • Download
  • " + + "
  • Delete
  • "; + } + + function updateProgressBars($progressBar, value) { + $progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%'); + $progressBar.find('.sr-only').html(data.status.recoveryProgress + "% Complete"); + } + + // before we add any new rows and update existing ones + // remove our flag for active rows + $('.' + ACTIVE_BACKUP_ROW_CLASS).removeClass(ACTIVE_BACKUP_ROW_CLASS); + + function updateOrAddTableRow(backup, tableBodyID) { + // check for a backup with this ID + var $backupRow = $("tr[data-backup-id='" + backup.id + "']"); + + if ($backupRow.length == 0) { + // create a new row and then add it to the table + $backupRow = $(createBackupTableRow(backup)); + $('#' + tableBodyID).append($backupRow); + } + + // update the row status column depending on if it is available or recovering + if (!backup.isAvailable) { + // add a progress bar to the status row for availability + $backupRow.find('td.backup-status').html(progressBarHTML('availability', 'Archiving')); + + // set the value of the progress bar based on availability progress + updateProgressBars($backupRow.find('.progress-bar'), backup.availabilityProgress * 100); + } else if (backup.id == data.status.recoveringBackupId) { + // add a progress bar to the status row for recovery + $backupRow.find('td.backup-status').html(progressBarHTML('recovery', 'Restoring')); + } else { + // no special status for this row, use an empty status column + $backupRow.find('td.backup-status').html(''); + } + + $backupRow.find('td.' + ACTION_MENU_CLASS + ' .dropdown').toggle(backup.isAvailable); + + $backupRow.addClass(ACTIVE_BACKUP_ROW_CLASS); } var automaticRows = ""; if (automaticBackups.length > 0) { for (var backupIndex in automaticBackups) { - // create a table row for this backup and add it to the rows we'll put in the table body - automaticRows += createBackupTableRow(automaticBackups[backupIndex]); + updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID); + } } - $('#' + AUTOMATIC_ARCHIVES_TBODY_ID).html(automaticRows); - - var manualRows = ""; - if (manualBackups.length > 0) { for (var backupIndex in manualBackups) { - // create a table row for this backup and add it to the rows we'll put in the table body - manualRows += createBackupTableRow(manualBackups[backupIndex]); + updateOrAddTableRow(manualBackups[backupIndex], MANUAL_ARCHIVES_TBODY_ID); } } - $('#' + MANUAL_ARCHIVES_TBODY_ID).html(manualRows); + // at this point, any rows that no longer have the ACTIVE_BACKUP_ROW_CLASS + // are deleted backups, so we remove them from the table + $('#' + CONTENT_ARCHIVES_NORMAL_ID + ' tbody tr:not(.' + ACTIVE_BACKUP_ROW_CLASS + ')').remove(); + + // check if the restore action on all rows should be enabled or disabled + $('.' + BACKUP_RESTORE_LINK_CLASS).parent().toggleClass('disabled', data.status.isRecovering); + + // hide or show the manual content upload file and button depending on our recovering status + $('#' + UPLOAD_CONTENT_ALLOWED_DIV_ID).toggle(!data.status.isRecovering); + $('#' + UPLOAD_CONTENT_RECOVERING_DIV_ID).toggle(data.status.isRecovering); + + // update the progress bars for current restore status + if (data.status.isRecovering) { + updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress * 100); + } // tell bootstrap sortable to update for the new rows $.bootstrapSortable({ applyLast: true }); + $('#' + CONTENT_ARCHIVES_NORMAL_ID).toggle(true); + $('#' + CONTENT_ARCHIVES_ERROR_ID).toggle(false); + }).fail(function(){ // we've hit the very rare case where we couldn't load the list of backups from the domain server @@ -167,11 +240,8 @@ $(document).ready(function(){ // replace the content archives panel with a simple error message // stating that the user should reload the page - $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html( - "
    " + - "There was a problem loading your list of automatic and manual content archives. Please reload the page to try again." + - "
    " - ); + $('#' + CONTENT_ARCHIVES_NORMAL_ID).toggle(false); + $('#' + CONTENT_ARCHIVES_ERROR_ID).toggle(true); }).always(function(){ // toggle showing or hiding the tables depending on if they have entries @@ -197,12 +267,11 @@ $(document).ready(function(){ "Restore content", function() { // show a spinner while we send off our request - showSpinnerAlert("Restoring Content Archive " + backupName); + showSpinnerAlert("Starting restore of " + backupName); // setup an AJAX POST to request content restore $.post('/api/backups/recover/' + backupID).done(function(data, textStatus, jqXHR) { swal.close(); - showRestartModal(); }).fail(function(jqXHR, textStatus, errorThrown) { showErrorMessage( "Error", @@ -247,7 +316,7 @@ $(document).ready(function(){ }).always(function(){ // reload the list of content archives in case we deleted a backup // or it's no longer an available backup for some other reason - reloadLatestBackups(); + reloadBackupInformation(); }); } ) @@ -306,7 +375,7 @@ $(document).ready(function(){ }).done(function(data) { // since we successfully setup a new content archive, reload the table of archives // which should show that this archive is pending creation - reloadLatestBackups(); + reloadBackupInformation(); }).fail(function(jqXHR, textStatus, errorThrown) { }); @@ -322,6 +391,9 @@ $(document).ready(function(){ setupContentArchives(); // load the latest backups immediately - reloadLatestBackups(); + reloadBackupInformation(); + + // setup a timer to reload them every 5 seconds + setInterval(reloadBackupInformation, 5000); }; }); diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 2bcc870ecf..62f442584e 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -466,6 +466,11 @@ tr.gray-tr { background-color: #f5f5f5; } +table .action-menu { + text-align: right; + width: 90px; +} + .dropdown-toggle span.glyphicon-option-vertical { font-size: 110%; cursor: pointer; diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index 3476792222..bafe5f96cf 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -263,7 +263,7 @@ $(document).ready(function(){ } }); - $('#' + Settings.FORM_ID).on('change keyup paste', '.' + Settings.TRIGGER_CHANGE_CLASS , function(e){ + $('#' + Settings.FORM_ID).on('change input propertychange', '.' + Settings.TRIGGER_CHANGE_CLASS , function(e){ // this input was changed, add the changed data attribute to it $(this).attr('data-changed', true); @@ -838,7 +838,7 @@ function addTableRow(row) { var keyInput = row.children(".key").children("input"); // whenever the keyInput changes, re-badge for differences - keyInput.on('change keyup paste', function(e){ + keyInput.on('change input propertychange', function(e){ // update siblings in the row to have the correct name var currentKey = $(this).val(); diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 379aa640f8..0bef6bb891 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -57,6 +57,8 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire _persistInterval(persistInterval), _lastCheck(usecTimestampNow()) { + setObjectName("DomainContentBackupManager"); + // Make sure the backup directory exists. QDir(_backupDirectory).mkpath("."); @@ -309,6 +311,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, } void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup) { + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "recoverFromUploadedBackup", Q_ARG(MiniPromise::Promise, promise), Q_ARG(QByteArray, uploadedBackup)); diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 3aab7b4563..ac94d953d5 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -435,10 +435,11 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectPastMaxCapacity) && !isWithinMaxCapacity()) { // we can't allow this user to connect because we are at max capacity QString redirectOnMaxCapacity; - const QVariant* redirectOnMaxCapacityVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION); - if (redirectOnMaxCapacityVariant && redirectOnMaxCapacityVariant->canConvert()) { - redirectOnMaxCapacity = redirectOnMaxCapacityVariant->toString(); + + QVariant redirectOnMaxCapacityVariant = + _server->_settingsManager.valueForKeyPath(MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION); + if (redirectOnMaxCapacityVariant.canConvert()) { + redirectOnMaxCapacity = redirectOnMaxCapacityVariant.toString(); qDebug() << "Redirection domain:" << redirectOnMaxCapacity; } @@ -610,9 +611,9 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, bool DomainGatekeeper::isWithinMaxCapacity() { // find out what our maximum capacity is - const QVariant* maximumUserCapacityVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); - unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; + QVariant maximumUserCapacityVariant = + _server->_settingsManager.valueForKeyPath(MAXIMUM_USER_CAPACITY); + unsigned int maximumUserCapacity = !maximumUserCapacityVariant.isValid() ? maximumUserCapacityVariant.toUInt() : 0; if (maximumUserCapacity > 0) { unsigned int connectedUsers = _server->countConnectedUsers(); diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index eee5673af3..24d55d74b6 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -84,21 +84,22 @@ void DomainMetadata::descriptorsChanged() { // get descriptors assert(_metadata[DESCRIPTORS].canConvert()); auto& state = *static_cast(_metadata[DESCRIPTORS].data()); - auto& settings = static_cast(parent())->_settingsManager.getSettingsMap(); - auto& descriptors = static_cast(parent())->_settingsManager.getDescriptorsMap(); + + static const QString DESCRIPTORS_GROUP_KEYPATH = "descriptors"; + auto descriptorsMap = static_cast(parent())->_settingsManager.valueForKeyPath(DESCRIPTORS).toMap(); // copy simple descriptors (description/maturity) - state[Descriptors::DESCRIPTION] = descriptors[Descriptors::DESCRIPTION]; - state[Descriptors::MATURITY] = descriptors[Descriptors::MATURITY]; + state[Descriptors::DESCRIPTION] = descriptorsMap[Descriptors::DESCRIPTION]; + state[Descriptors::MATURITY] = descriptorsMap[Descriptors::MATURITY]; // copy array descriptors (hosts/tags) - state[Descriptors::HOSTS] = descriptors[Descriptors::HOSTS].toList(); - state[Descriptors::TAGS] = descriptors[Descriptors::TAGS].toList(); + state[Descriptors::HOSTS] = descriptorsMap[Descriptors::HOSTS].toList(); + state[Descriptors::TAGS] = descriptorsMap[Descriptors::TAGS].toList(); // parse capacity static const QString CAPACITY = "security.maximum_user_capacity"; - const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); - unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; + QVariant capacityVariant = static_cast(parent())->_settingsManager.valueForKeyPath(CAPACITY); + unsigned int capacity = capacityVariant.isValid() ? capacityVariant.toUInt() : 0; state[Descriptors::CAPACITY] = capacity; #if DEV_BUILD || PR_BUILD diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7cd6cd34fe..9cecea5f70 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -75,8 +75,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, std::initializer_list optionalData, bool requireAccessToken) { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (accessTokenVariant == nullptr && requireAccessToken) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid() && requireAccessToken) { connection->respond(HTTPConnection::StatusCode400, "User access token has not been set"); return true; } @@ -112,8 +112,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - if (accessTokenVariant != nullptr) { - auto accessTokenHeader = QString("Bearer ") + accessTokenVariant->toString(); + if (accessTokenVariant.isValid()) { + auto accessTokenHeader = QString("Bearer ") + accessTokenVariant.toString(); req.setRawHeader("Authorization", accessTokenHeader.toLatin1()); } @@ -380,6 +380,11 @@ void DomainServer::parseCommandLine() { DomainServer::~DomainServer() { qInfo() << "Domain Server is shutting down."; + if (_contentManager) { + _contentManager->aboutToFinish(); + _contentManager->terminate(); + } + // cleanup the AssetClient thread DependencyManager::destroy(); _assetClientThread.quit(); @@ -387,11 +392,6 @@ DomainServer::~DomainServer() { // destroy the LimitedNodeList before the DomainServer QCoreApplication is down DependencyManager::destroy(); - - if (_contentManager) { - _contentManager->aboutToFinish(); - _contentManager->terminate(); - } } void DomainServer::queuedQuit(QString quitMessage, int exitCode) { @@ -417,8 +417,8 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_PRIVATE_KEY_OPTION = "key"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; - QString certPath = _settingsManager.getSettingsMap().value(X509_CERTIFICATE_OPTION).toString(); - QString keyPath = _settingsManager.getSettingsMap().value(X509_PRIVATE_KEY_OPTION).toString(); + QString certPath = _settingsManager.valueForKeyPath(X509_CERTIFICATE_OPTION).toString(); + QString keyPath = _settingsManager.valueForKeyPath(X509_PRIVATE_KEY_OPTION).toString(); if (!certPath.isEmpty() && !keyPath.isEmpty()) { // the user wants to use the following cert and key for HTTPS @@ -461,8 +461,7 @@ bool DomainServer::optionallySetupOAuth() { const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; const QString REDIRECT_HOSTNAME_OPTION = "hostname"; - const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - _oauthProviderURL = QUrl(settingsMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); + _oauthProviderURL = QUrl(_settingsManager.valueForKeyPath(OAUTH_PROVIDER_URL_OPTION).toString()); // if we don't have an oauth provider URL then we default to the default node auth url if (_oauthProviderURL.isEmpty()) { @@ -472,9 +471,9 @@ bool DomainServer::optionallySetupOAuth() { auto accountManager = DependencyManager::get(); accountManager->setAuthURL(_oauthProviderURL); - _oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString(); + _oauthClientID = _settingsManager.valueForKeyPath(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); - _hostname = settingsMap.value(REDIRECT_HOSTNAME_OPTION).toString(); + _hostname = _settingsManager.valueForKeyPath(REDIRECT_HOSTNAME_OPTION).toString(); if (!_oauthClientID.isEmpty()) { if (_oauthProviderURL.isEmpty() @@ -499,11 +498,11 @@ static const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; void DomainServer::getTemporaryName(bool force) { // check if we already have a domain ID - const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); + QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH); qInfo() << "Requesting temporary domain name"; - if (idValueVariant) { - qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant->toString(); + if (idValueVariant.isValid()) { + qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant.toString(); if (force) { qDebug() << "Requesting temporary domain name to replace current ID:" << getID(); } else { @@ -543,9 +542,6 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { auto settingsDocument = QJsonDocument::fromJson(newSettingsJSON.toUtf8()); _settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object(), DomainSettings); - // store the new ID and auto networking setting on disk - _settingsManager.persistToFile(); - // store the new token to the account info auto accountManager = DependencyManager::get(); accountManager->setTemporaryDomain(id, key); @@ -647,8 +643,6 @@ void DomainServer::setupNodeListAndAssignments() { QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION); int domainServerPort = localPortValue.toInt(); - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - int domainServerDTLSPort = INVALID_PORT; if (_isUsingDTLS) { @@ -656,8 +650,9 @@ void DomainServer::setupNodeListAndAssignments() { const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port"; - if (settingsMap.contains(CUSTOM_DTLS_PORT_OPTION)) { - domainServerDTLSPort = (unsigned short) settingsMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt(); + auto dtlsPortVariant = _settingsManager.valueForKeyPath(CUSTOM_DTLS_PORT_OPTION); + if (dtlsPortVariant.isValid()) { + domainServerDTLSPort = (unsigned short) dtlsPortVariant.toUInt(); } } @@ -687,9 +682,9 @@ void DomainServer::setupNodeListAndAssignments() { nodeList->setSessionUUID(_overridingDomainID); isMetaverseDomain = true; // assume metaverse domain } else { - const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); - if (idValueVariant) { - nodeList->setSessionUUID(idValueVariant->toString()); + QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH); + if (idValueVariant.isValid()) { + nodeList->setSessionUUID(idValueVariant.toString()); isMetaverseDomain = true; // if we have an ID, we'll assume we're a metaverse domain } else { nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID @@ -758,10 +753,10 @@ bool DomainServer::resetAccountManagerAccessToken() { QString accessToken = QProcessEnvironment::systemEnvironment().value(ENV_ACCESS_TOKEN_KEY); if (accessToken.isEmpty()) { - const QVariant* accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); + QVariant accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); - if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) { - accessToken = accessTokenVariant->toString(); + if (accessTokenVariant.isValid() && accessTokenVariant.canConvert(QMetaType::QString)) { + accessToken = accessTokenVariant.toString(); } else { qWarning() << "No access token is present. Some operations that use the metaverse API will fail."; qDebug() << "Set an access token via the web interface, in your user config" @@ -892,31 +887,26 @@ void DomainServer::updateICEServerAddresses() { } void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) { - const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)"; - QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING); - - const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + const QString ASSIGNMENT_CONFIG_PREFIX = "config-"; // scan for assignment config keys - QStringList variantMapKeys = settingsMap.keys(); - int configIndex = variantMapKeys.indexOf(assignmentConfigRegex); + for (int i = 0; i < Assignment::AllTypes; ++i) { + QVariant assignmentConfigVariant = _settingsManager.valueOrDefaultValueForKeyPath(ASSIGNMENT_CONFIG_PREFIX + QString::number(i)); - while (configIndex != -1) { - // figure out which assignment type this matches - Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt(); + if (assignmentConfigVariant.isValid()) { + // figure out which assignment type this matches + Assignment::Type assignmentType = static_cast(i); - if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) { - QVariant mapValue = settingsMap[variantMapKeys[configIndex]]; - QVariantList assignmentList = mapValue.toList(); + if (!excludedTypes.contains(assignmentType)) { + QVariantList assignmentList = assignmentConfigVariant.toList(); - if (assignmentType != Assignment::AgentType) { - createStaticAssignmentsForType(assignmentType, assignmentList); + if (assignmentType != Assignment::AgentType) { + createStaticAssignmentsForType(assignmentType, assignmentList); + } + + excludedTypes.insert(assignmentType); } - - excludedTypes.insert(assignmentType); } - - configIndex = variantMapKeys.indexOf(assignmentConfigRegex, configIndex + 1); } } @@ -928,10 +918,10 @@ void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment void DomainServer::populateStaticScriptedAssignmentsFromSettings() { const QString PERSISTENT_SCRIPTS_KEY_PATH = "scripts.persistent_scripts"; - const QVariant* persistentScriptsVariant = valueForKeyPath(_settingsManager.getSettingsMap(), PERSISTENT_SCRIPTS_KEY_PATH); + QVariant persistentScriptsVariant = _settingsManager.valueOrDefaultValueForKeyPath(PERSISTENT_SCRIPTS_KEY_PATH); - if (persistentScriptsVariant) { - QVariantList persistentScriptsList = persistentScriptsVariant->toList(); + if (persistentScriptsVariant.isValid()) { + QVariantList persistentScriptsList = persistentScriptsVariant.toList(); foreach(const QVariant& persistentScriptVariant, persistentScriptsList) { QVariantMap persistentScript = persistentScriptVariant.toMap(); @@ -1761,7 +1751,7 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointer(); - auto getSetting = [this](QString keyPath, QVariant& value) -> bool { - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - QVariant* var = valueForKeyPath(settingsMap, keyPath); - if (var == nullptr) { + auto getSetting = [this](QString keyPath, QVariant value) -> bool { + + value = _settingsManager.valueForKeyPath(keyPath); + if (!value.isValid()) { return false; } - value = *var; return true; }; @@ -2028,8 +2017,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { const QString URI_WIZARD = "/wizard/"; const QString WIZARD_COMPLETED_ONCE_KEY_PATH = "wizard.completed_once"; - const QVariant* wizardCompletedOnce = valueForKeyPath(_settingsManager.getSettingsMap(), WIZARD_COMPLETED_ONCE_KEY_PATH); - const bool completedOnce = wizardCompletedOnce && wizardCompletedOnce->toBool(); + QVariant wizardCompletedOnce = _settingsManager.valueForKeyPath(WIZARD_COMPLETED_ONCE_KEY_PATH); + const bool completedOnce = wizardCompletedOnce.isValid() && wizardCompletedOnce.toBool(); if (url.path() != URI_WIZARD && url.path().endsWith('/') && !completedOnce) { // First visit, redirect to the wizard @@ -2326,8 +2315,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path() == "/domain_settings") { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (!accessTokenVariant) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid()) { connection->respond(HTTPConnection::StatusCode400); return true; } @@ -2360,8 +2349,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return forwardMetaverseAPIRequest(connection, "/api/v1/domains/" + domainID, "domain", { }, { "network_address", "network_port", "label" }); } else if (url.path() == URI_API_PLACES) { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (!accessTokenVariant->isValid()) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid()) { connection->respond(HTTPConnection::StatusCode400, "User access token has not been set"); return true; } @@ -2409,7 +2398,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QUrl url { NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/api/v1/places/" + place_id }; - url.setQuery("access_token=" + accessTokenVariant->toString()); + url.setQuery("access_token=" + accessTokenVariant.toString()); QNetworkRequest req(url); req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); @@ -2604,10 +2593,11 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server."; - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY); + QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY); if (!_oauthProviderURL.isEmpty() - && (settingsMap.contains(ADMIN_USERS_CONFIG_KEY) || settingsMap.contains(ADMIN_ROLES_CONFIG_KEY))) { + && (adminUsersVariant.isValid() || adminRolesVariant.isValid())) { QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY); const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; @@ -2618,7 +2608,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl cookieUUID = cookieUUIDRegex.cap(1); } - if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { + if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication." << "These cannot be combined - using OAuth for authentication."; } @@ -2628,13 +2618,13 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID); QString profileUsername = sessionData.getUsername(); - if (settingsMap.value(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) { + if (_settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) { // this is an authenticated user return true; } // loop the roles of this user and see if they are in the admin-roles array - QStringList adminRolesArray = settingsMap.value(ADMIN_ROLES_CONFIG_KEY).toStringList(); + QStringList adminRolesArray = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY).toStringList(); if (!adminRolesArray.isEmpty()) { foreach(const QString& userRole, sessionData.getRoles()) { @@ -2679,7 +2669,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl // we don't know about this user yet, so they are not yet authenticated return false; } - } else if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { + } else if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { // config file contains username and password combinations for basic auth const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization"; @@ -2698,10 +2688,10 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl QString headerPassword = credentialList[1]; // we've pulled a username and password - now check if there is a match in our basic auth hash - QString settingsUsername = valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)->toString(); - const QVariant* settingsPasswordVariant = valueForKeyPath(settingsMap, BASIC_AUTH_PASSWORD_KEY_PATH); + QString settingsUsername = _settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).toString(); + QVariant settingsPasswordVariant = _settingsManager.valueForKeyPath(BASIC_AUTH_PASSWORD_KEY_PATH); - QString settingsPassword = settingsPasswordVariant ? settingsPasswordVariant->toString() : ""; + QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : ""; QString hexHeaderPassword = headerPassword.isEmpty() ? "" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex(); @@ -2838,13 +2828,14 @@ ReplicationServerInfo serverInformationFromSettings(QVariantMap serverMap, Repli } void DomainServer::updateReplicationNodes(ReplicationServerDirection direction) { - auto settings = _settingsManager.getSettingsMap(); - if (settings.contains(BROADCASTING_SETTINGS_KEY)) { + auto broadcastSettingsVariant = _settingsManager.valueForKeyPath(BROADCASTING_SETTINGS_KEY); + + if (broadcastSettingsVariant.isValid()) { auto nodeList = DependencyManager::get(); std::vector replicationNodesInSettings; - auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap(); + auto replicationSettings = broadcastSettingsVariant.toMap(); QString serversKey = direction == Upstream ? "upstream_servers" : "downstream_servers"; QString replicationDirection = direction == Upstream ? "upstream" : "downstream"; @@ -2920,13 +2911,12 @@ void DomainServer::updateUpstreamNodes() { void DomainServer::updateReplicatedNodes() { // Make sure we have downstream nodes in our list - auto settings = _settingsManager.getSettingsMap(); - static const QString REPLICATED_USERS_KEY = "users"; _replicatedUsernames.clear(); - - if (settings.contains(BROADCASTING_SETTINGS_KEY)) { - auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap(); + + auto replicationVariant = _settingsManager.valueForKeyPath(BROADCASTING_SETTINGS_KEY); + if (replicationVariant.isValid()) { + auto replicationSettings = replicationVariant.toMap(); if (replicationSettings.contains(REPLICATED_USERS_KEY)) { auto usersSettings = replicationSettings.value(REPLICATED_USERS_KEY).toList(); for (auto& username : usersSettings) { @@ -3114,17 +3104,17 @@ void DomainServer::processPathQueryPacket(QSharedPointer messag // check out paths in the _configMap to see if we have a match auto keypath = QString(PATHS_SETTINGS_KEYPATH_FORMAT).arg(SETTINGS_PATHS_KEY).arg(pathQuery); - const QVariant* pathMatch = valueForKeyPath(_settingsManager.getSettingsMap(), keypath); + QVariant pathMatch = _settingsManager.valueForKeyPath(keypath); - if (pathMatch || pathQuery == INDEX_PATH) { + if (pathMatch.isValid() || pathQuery == INDEX_PATH) { // we got a match, respond with the resulting viewpoint auto nodeList = DependencyManager::get(); QString responseViewpoint; // if we didn't match the path BUT this is for the index path then send back our default - if (pathMatch) { - responseViewpoint = pathMatch->toMap()[PATH_VIEWPOINT_KEY].toString(); + if (pathMatch.isValid()) { + responseViewpoint = pathMatch.toMap()[PATH_VIEWPOINT_KEY].toString(); } else { const QString DEFAULT_INDEX_PATH = "/0,0,0/0,0,0,1"; responseViewpoint = DEFAULT_INDEX_PATH; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index a3f99facea..5157654c33 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -38,6 +38,9 @@ #include "DomainServerNodeData.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; +const QString SETTINGS_PATH = "/settings"; +const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; +const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json"; const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; @@ -190,6 +193,9 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer(getSettingsMap()[DESCRIPTORS].data()); -} - void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& permissionsRows, QString groupName, NodePermissionsPointer perms) { // this is called when someone has used the domain-settings webpage to add a group. They type the group's name @@ -487,6 +482,9 @@ void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& void DomainServerSettingsManager::packPermissionsForMap(QString mapName, NodePermissionsMap& permissionsRows, QString keyPath) { + // grab a write lock on the settings mutex since we're about to change the config map + QWriteLocker locker(&_settingsLock); + // find (or create) the "security" section of the settings map QVariant* security = _configMap.valueForKeyPath("security", true); if (!security->canConvert(QMetaType::QVariantMap)) { @@ -576,15 +574,20 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key mapPointer->clear(); - QVariant* permissions = _configMap.valueForKeyPath(keyPath, true); - if (!permissions->canConvert(QMetaType::QVariantList)) { + QVariant permissions = valueOrDefaultValueForKeyPath(keyPath); + + if (!permissions.isValid()) { + // we don't have a permissions object to unpack for this keypath, bail + return false; + } + + if (!permissions.canConvert(QMetaType::QVariantList)) { qDebug() << "Failed to extract permissions for key path" << keyPath << "from settings."; - (*permissions) = QVariantList(); } bool needPack = false; - QList permissionsList = permissions->toList(); + QList permissionsList = permissions.toList(); foreach (QVariant permsHash, permissionsList) { NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; QString id = perms->getID(); @@ -611,6 +614,11 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key void DomainServerSettingsManager::unpackPermissions() { // transfer details from _configMap to _agentPermissions + // NOTE: Defaults for standard permissions (anonymous, friends, localhost, logged-in) used + // to be set here and then immediately persisted to the config JSON file. + // They have since been moved to describe-settings.json as the default value for AGENT_STANDARD_PERMISSIONS_KEYPATH. + // In order to change the default standard permissions you must change the default value in describe-settings.json. + bool needPack = false; needPack |= unpackPermissionsForKeypath(AGENT_STANDARD_PERMISSIONS_KEYPATH, &_standardAgentPermissions); @@ -670,57 +678,39 @@ void DomainServerSettingsManager::unpackPermissions() { } }); - // if any of the standard names are missing, add them - foreach(const QString& standardName, NodePermissions::standardNames) { - NodePermissionsKey standardKey { standardName, 0 }; - if (!_standardAgentPermissions.contains(standardKey)) { - // we don't have permissions for one of the standard groups, so we'll add them now - NodePermissionsPointer perms { new NodePermissions(standardKey) }; - - if (standardKey == NodePermissions::standardNameLocalhost) { - // the localhost user is granted all permissions by default - perms->setAll(true); - } else { - // anonymous, logged in, and friend users get connect permissions by default - perms->set(NodePermissions::Permission::canConnectToDomain); - perms->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities); - } - - // add the permissions to the standard map - _standardAgentPermissions[standardKey] = perms; - - // this will require a packing of permissions - needPack = true; - } - } - needPack |= ensurePermissionsForGroupRanks(); if (needPack) { packPermissions(); } - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG qDebug() << "--------------- permissions ---------------------"; - QList> permissionsSets; - permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get() - << _groupPermissions.get() << _groupForbiddens.get() - << _ipPermissions.get() << _macPermissions.get() - << _machineFingerprintPermissions.get(); + std::list permissionsSets { + &_standardAgentPermissions, &_agentPermissions, + &_groupPermissions, &_groupForbiddens, + &_ipPermissions, &_macPermissions, + &_machineFingerprintPermissions + }; foreach (auto permissionSet, permissionsSets) { - QHashIterator i(permissionSet); - while (i.hasNext()) { - i.next(); - NodePermissionsPointer perms = i.value(); + auto& permissionKeyMap = permissionSet->get(); + auto it = permissionKeyMap.begin(); + + while (it != permissionKeyMap.end()) { + + NodePermissionsPointer perms = it->second; if (perms->isGroup()) { - qDebug() << i.key() << perms->getGroupID() << perms; + qDebug() << it->first << perms->getGroupID() << perms; } else { - qDebug() << i.key() << perms; + qDebug() << it->first << perms; } + + ++it; } } - #endif +#endif + } bool DomainServerSettingsManager::ensurePermissionsForGroupRanks() { @@ -1068,12 +1058,22 @@ NodePermissions DomainServerSettingsManager::getForbiddensForGroup(const QUuid& return getForbiddensForGroup(groupKey.first, groupKey.second); } +QVariant DomainServerSettingsManager::valueForKeyPath(const QString& keyPath) { + QReadLocker locker(&_settingsLock); + auto foundValue = _configMap.valueForKeyPath(keyPath); + return foundValue ? *foundValue : QVariant(); +} + QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) { + QReadLocker locker(&_settingsLock); const QVariant* foundValue = _configMap.valueForKeyPath(keyPath); if (foundValue) { return *foundValue; } else { + // we don't need the settings lock anymore since we're done reading from the config map + locker.unlock(); + int dotIndex = keyPath.indexOf('.'); QString groupKey = keyPath.mid(0, dotIndex); @@ -1112,9 +1112,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection // we recurse one level deep below each group for the appropriate setting bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject, endpointType); - // store whatever the current _settingsMap is to file - persistToFile(); - // return success to the caller QString jsonSuccess = "{\"status\": \"success\"}"; connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); @@ -1216,16 +1213,9 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) { - - if (thread() != QThread::currentThread()) { - bool success; - BLOCKING_INVOKE_METHOD(this, "restoreSettingsFromObject", - Q_RETURN_ARG(bool, success), - Q_ARG(QJsonObject, settingsToRestore), - Q_ARG(SettingsType, settingsType)); - return success; - } + // grab a write lock since we're about to change the settings map + QWriteLocker locker(&_settingsLock); QJsonArray* filteredDescriptionArray = settingsType == DomainSettings ? &_domainSettingsDescription : &_contentSettingsDescription; @@ -1341,6 +1331,10 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings } else { // restore completed, persist the new settings qDebug() << "Restore completed, persisting restored settings to file"; + + // let go of the write lock since we're done making changes to the config map + locker.unlock(); + persistToFile(); return true; } @@ -1352,20 +1346,6 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt bool includeDefaults, bool isForBackup) { QJsonObject responseObject; - if (thread() != QThread::currentThread()) { - - BLOCKING_INVOKE_METHOD(this, "settingsResponseObjectForType", - Q_RETURN_ARG(QJsonObject, responseObject), - Q_ARG(const QString&, typeValue), - Q_ARG(bool, isAuthenticated), - Q_ARG(bool, includeDomainSettings), - Q_ARG(bool, includeContentSettings), - Q_ARG(bool, includeDefaults), - Q_ARG(bool, isForBackup)); - - return responseObject; - } - if (!typeValue.isEmpty() || isAuthenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); @@ -1374,6 +1354,7 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt // only enumerate the requested settings type (domain setting or content setting) QJsonArray* filteredDescriptionArray = &_descriptionArray; + if (includeDomainSettings && !includeContentSettings) { filteredDescriptionArray = &_domainSettingsDescription; } else if (includeContentSettings && !includeDomainSettings) { @@ -1413,21 +1394,21 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt QVariant variantValue; if (!groupKey.isEmpty()) { - QVariant settingsMapGroupValue = _configMap.value(groupKey); + QVariant settingsMapGroupValue = valueForKeyPath(groupKey); if (!settingsMapGroupValue.isNull()) { variantValue = settingsMapGroupValue.toMap().value(settingName); } } else { - variantValue = _configMap.value(settingName); + variantValue = valueForKeyPath(settingName); } // final check for inclusion // either we include default values or we don't but this isn't a default value - if (includeDefaults || !variantValue.isNull()) { + if (includeDefaults || variantValue.isValid()) { QJsonValue result; - if (variantValue.isNull()) { + if (!variantValue.isValid()) { // no value for this setting, pass the default if (settingObject.contains(SETTING_DEFAULT_KEY)) { result = settingObject[SETTING_DEFAULT_KEY]; @@ -1566,6 +1547,10 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType) { + + // take a write lock since we're about to overwrite settings in the config map + QWriteLocker locker(&_settingsLock); + static const QString SECURITY_ROOT_KEY = "security"; static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist"; static const QString BROADCASTING_KEY = "broadcasting"; @@ -1663,6 +1648,12 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ } } + // we're done making changes to the config map, let go of our read lock + locker.unlock(); + + // store whatever the current config map is to file + persistToFile(); + return needRestart; } @@ -1689,6 +1680,9 @@ bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) { } void DomainServerSettingsManager::sortPermissions() { + // take a write lock since we're about to change the config map data + QWriteLocker locker(&_settingsLock); + // sort the permission-names QVariant* standardPermissions = _configMap.valueForKeyPath(AGENT_STANDARD_PERMISSIONS_KEYPATH); if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) { @@ -1725,11 +1719,15 @@ void DomainServerSettingsManager::persistToFile() { QFile settingsFile(_configMap.getUserConfigFilename()); if (settingsFile.open(QIODevice::WriteOnly)) { + // take a read lock so we can grab the config and write it to file + QReadLocker locker(&_settingsLock); settingsFile.write(QJsonDocument::fromVariant(_configMap.getConfig()).toJson()); } else { qCritical("Could not write to JSON settings file. Unable to persist settings."); // failed to write, reload whatever the current config state is + // with a write lock since we're about to overwrite the config map + QWriteLocker locker(&_settingsLock); _configMap.loadConfig(_argumentList); } } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 897a15485f..d81547410b 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -27,9 +27,6 @@ const QString SETTINGS_PATHS_KEY = "paths"; -const QString SETTINGS_PATH = "/settings"; -const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; -const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json"; const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions"; @@ -53,11 +50,12 @@ public: bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url); void setupConfigMap(const QStringList& argumentList); + + // each of the three methods in this group takes a read lock of _settingsLock + // and cannot be called when the a write lock is held by the same thread QVariant valueOrDefaultValueForKeyPath(const QString& keyPath); - - QVariantMap& getSettingsMap() { return _configMap.getConfig(); } - - QVariantMap& getDescriptorsMap(); + QVariant valueForKeyPath(const QString& keyPath); + bool containsKeyPath(const QString& keyPath) { return valueForKeyPath(keyPath).isValid(); } // these give access to anonymous/localhost/logged-in settings from the domain-server settings page bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, 0); } @@ -119,6 +117,8 @@ public: /// thread safe method to restore settings from a JSON object Q_INVOKABLE bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); + bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType); + signals: void updateNodePermissions(); void settingsUpdated(); @@ -138,12 +138,13 @@ private: QStringList _argumentList; QJsonArray filteredDescriptionArray(bool isContentSettings); - bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType); - void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName); void sortPermissions(); + + // you cannot be holding the _settingsLock when persisting to file from the same thread + // since it may take either a read lock or write lock and recursive locking doesn't allow a change in type void persistToFile(); void splitSettingsDescription(); @@ -155,10 +156,10 @@ private: QJsonArray _contentSettingsDescription; QJsonObject _settingsMenuGroups; + // any method that calls _valueForKeyPath on this _configMap must get a write lock it keeps until it + // is done with the returned QVariant* HifiConfigVariantMap _configMap; - friend class DomainServer; - // these cause calls to metaverse's group api void apiGetGroupID(const QString& groupName); void apiGetGroupRanks(const QUuid& groupID); @@ -192,6 +193,9 @@ private: // keep track of answers to api queries about which users are in which groups QHash> _groupMembership; // QHash> + + /// guard read/write access from multiple threads to settings + QReadWriteLock _settingsLock { QReadWriteLock::Recursive }; }; #endif // hifi_DomainServerSettingsManager_h