From affeb720b008a27c0c76dfe06908cbe3eacfe20f Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Mon, 10 Mar 2025 08:45:35 -0500 Subject: [PATCH 1/7] Initial support. --- .../resources/describe-settings.json | 56 ++++++++++++------- .../resources/web/js/base-settings.js | 11 ++++ .../resources/web/settings/js/settings.js | 40 +++++++------ domain-server/src/DomainServer.cpp | 36 ++++++++++-- 4 files changed, 99 insertions(+), 44 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 5b1c3482c12..c69011f01d9 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -326,26 +326,42 @@ "restart": false, "settings": [ { - "name": "http_username", - "label": "HTTP Username", - "help": "Username used for basic HTTP authentication.", - "backup": false - }, - { - "name": "http_password", - "label": "HTTP Password", - "type": "password", - "help": "Password used for basic HTTP authentication. Leave this alone if you do not want to change it.", - "password_placeholder": "******", - "value-hidden": true, - "backup": false - }, - { - "name": "verify_http_password", - "label": "Verify HTTP Password", - "type": "password", - "help": "Must match the password entered above for change to be saved.", - "value-hidden": true + "name": "http_authentication", + "type": "table", + "caption": "Authorized Users for HTTP Basic authentication", + "help": "These are accounts that are authorized to sign in to the web interface.", + "can_add_new_categories": false, + "can_add_new_rows": true, + "new_category_placeholder": "Add new user", + "columns": [ + { + "name": "http_username", + "label": "Username", + "hidden": false, + "backup": false, + "readonly": false, + "editable": true + }, + { + "name": "http_password", + "label": "Password", + "type": "password", + "password_placeholder": "******", + "value-hidden": true, + "hidden": false, + "backup": false, + "readonly": false, + "editable": true + }, + { + "name": "http_password_verify", + "label": "Verify Password", + "type": "password", + "backup": false, + "readonly": false, + "editable": true + } + ] }, { "name": "approved_safe_urls", diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index d3f5baa2f94..1f5ff052a95 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -473,6 +473,7 @@ function makeTable(setting, keypath, setting_value) { var html = ""; + // Apply the "help" block above the table. if (setting.help) { html += "" + setting.help + "" } @@ -480,11 +481,13 @@ function makeTable(setting, keypath, setting_value) { var nonDeletableRowKey = setting["non-deletable-row-key"]; var nonDeletableRowValues = setting["non-deletable-row-values"]; + // Initialize the table. html += ""; + // Add a caption (if provided). if (setting.caption) { html += "" } @@ -508,10 +511,12 @@ function makeTable(setting, keypath, setting_value) { // Column names html += "" + // Add a number column (If enabled) if (setting.numbered === true) { html += "" // Row number } + // Add a key column (If enabled) if (setting.key) { html += "" // Key } @@ -585,6 +590,12 @@ function makeTable(setting, keypath, setting_value) { "" + ""; + } else if (isArray && col.type === "password" && col.editable) { + html += + ""; } else if (isArray && col.type === "time" && col.editable) { html += ""; } else if (isArray && col.type === "time" && col.editable) { html += From c315da003b4723e354e1f19a77932090f3b92484 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 11 Mar 2025 04:29:54 -0500 Subject: [PATCH 7/7] Update path for existing configs. --- .../resources/describe-settings.json | 2 +- .../src/DomainServerSettingsManager.cpp | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index c69011f01d9..12cb2182df4 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.7, + "version": 2.8, "settings": [ { "name": "metaverse", diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index fe13c29df21..79193cb781d 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -570,6 +570,33 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena packPermissions(); } + if (oldVersion < 2.8) { + // Turn HTTP authentication into the new array format that allows multiple accounts. + // https://github.com/overte-org/overte/pull/1350 + const QString HTTP_USERNAME = "security.http_username"; + const QString HTTP_PASSWORD = "security.http_password"; + const QString HTTP_AUTHENTICATION = "security.http_authentication"; + + QVariant* httpUsernameValue = _configMap.valueForKeyPath(HTTP_USERNAME); + QVariant* httpPasswordValue = _configMap.valueForKeyPath(HTTP_PASSWORD); + QVariant* httpAuthentication = _configMap.valueForKeyPath(HTTP_AUTHENTICATION, true); + + if (httpUsernameValue && httpUsernameValue->canConvert(QMetaType::QString)) { + qDebug() << "Migrating domain account to multi account friendly system."; + QJsonArray accountList; + QJsonObject accountListObject; + + accountListObject.insert("http_username", httpUsernameValue->toString()); + accountListObject.insert("http_password", httpPasswordValue->toString()); + + // Append the existing account to the account list object + accountList.append(accountListObject); + + // Set the array as the new value for http_authentication + *httpAuthentication = accountList; + } + } + // write the current description version to our settings *versionVariant = _descriptionVersion;
" + setting.caption + "
#" + setting.key.label + "" + + "" + + "" + diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index d3792cf36ec..4a746c8d489 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -77,30 +77,34 @@ $(document).ready(function(){ if (!verifyAvatarHeights()) { return false; } - - // check if we've set the basic http password - if (formJSON["security"]) { - var password = formJSON["security"]["http_password"]; - var verify_password = formJSON["security"]["verify_http_password"]; + // Check if we have updated the basic http authentication settings + if (formJSON["security"] && formJSON["security"]["http_authentication"]) { + for (loginIndex in formJSON["security"]["http_authentication"]) { + var loginPair = formJSON["security"]["http_authentication"][loginIndex]; - // if they've only emptied out the default password field, we should go ahead and acknowledge - // the verify password field - if (password != undefined && verify_password == undefined) { - verify_password = ""; - } + var password = loginPair["http_password"] || null; + var passwordVerify = loginPair["http_password_verify"] || null; - // if we have a password and its verification, convert it to sha256 for comparison - if (password != undefined && verify_password != undefined) { - formJSON["security"]["http_password"] = sha256_digest(password); - formJSON["security"]["verify_http_password"] = sha256_digest(verify_password); - - if (password == verify_password) { - delete formJSON["security"]["verify_http_password"]; - } else { + if (passwordVerify && password !== passwordVerify) { + // Tried to change the password, but the password does not match bootbox.alert({ "message": "Passwords must match!", "title": "Password Error" }); return false; } + + if (passwordVerify && password == passwordVerify) { + // If password and password verify match, we are changing the password for an account. + console.log(`Changing ${loginPair["http_username_multi"]}'s password`); + + // Hash the new password + password = sha256_digest(password); + } + + // Delete the verification field so we don't try and save it. + delete formJSON["security"]["http_authentication"][loginIndex]["http_password_verify"]; + + // Set the form password value to the new hashed value of that password + formJSON["security"]["http_authentication"][loginIndex]["http_password"] = password; } } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 988a21c899f..8303cb96447 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2834,8 +2834,7 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c static const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie"; static const QString ADMIN_USERS_CONFIG_KEY = "oauth.admin-users"; static const QString ADMIN_ROLES_CONFIG_KEY = "oauth.admin-roles"; - static const QString BASIC_AUTH_USERNAME_KEY_PATH = "security.http_username"; - static const QString BASIC_AUTH_PASSWORD_KEY_PATH = "security.http_password"; + static const QString BASIC_AUTH_MULTI_PATH = "security.http_authentication"; const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server."; @@ -2856,7 +2855,7 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c cookieUUID = cookieUUIDRegex.cap(1); } - if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { + if (_settingsManager.valueForKeyPath(BASIC_AUTH_MULTI_PATH).isValid()) { qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication." << "These cannot be combined - using OAuth for authentication."; } @@ -2926,7 +2925,7 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c // we don't know about this user yet, so they are not yet authenticated return { false, QString() }; } - } else if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { + } else if (_settingsManager.valueForKeyPath(BASIC_AUTH_MULTI_PATH).isValid()) { // config file contains username and password combinations for basic auth const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization"; @@ -2944,9 +2943,34 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c QString headerUsername = credentialList[0]; QString headerPassword = credentialList[1]; + QString settingsUsername; + QVariant settingsPasswordVariant; + // we've pulled a username and password - now check if there is a match in our basic auth hash - QString settingsUsername = _settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).toString(); - QVariant settingsPasswordVariant = _settingsManager.valueForKeyPath(BASIC_AUTH_PASSWORD_KEY_PATH); + // MUL TODO: Don't allow empty usernames. + // MUL TODO: Don't allow duplicate usernames. + // MUL TODO: Require at least one account at all times? + // MUL TODO: New config version! + // MUL TODO: Allow updating password on new value in password / password verify. + QVariant allAccounts = _settingsManager.valueForKeyPath(BASIC_AUTH_MULTI_PATH); + QList accountList = allAccounts.toList(); + + for (const QVariant &account : accountList) { + // Convert QVariant to QMap + QMap accountMap = account.toMap(); + + // Retrieve the values of the properties + QString httpUsername = accountMap.value("http_username_multi").toString(); + QString httpPassword = accountMap.value("http_password_multi").toString(); + + if (httpUsername == headerUsername) { + // Found the username we are looking for + settingsUsername = httpUsername; + settingsPasswordVariant = httpPassword; + + break; + } + } QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : ""; QString hexHeaderPassword = headerPassword.isEmpty() ? From 9c4d74c6a162db06c9f7d40de5b1b4f7b21321cb Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 11 Mar 2025 01:21:17 -0500 Subject: [PATCH 2/7] Remove main todo list. Check https://github.com/overte-org/overte/pull/1350 for up to date list. --- domain-server/src/DomainServer.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8303cb96447..a8d8293db31 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2947,11 +2947,6 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c QVariant settingsPasswordVariant; // we've pulled a username and password - now check if there is a match in our basic auth hash - // MUL TODO: Don't allow empty usernames. - // MUL TODO: Don't allow duplicate usernames. - // MUL TODO: Require at least one account at all times? - // MUL TODO: New config version! - // MUL TODO: Allow updating password on new value in password / password verify. QVariant allAccounts = _settingsManager.valueForKeyPath(BASIC_AUTH_MULTI_PATH); QList accountList = allAccounts.toList(); From b5e044123ed1045f3055ee9c9888cdd27181798c Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 11 Mar 2025 01:58:02 -0500 Subject: [PATCH 3/7] Prohibit empty usernames. --- domain-server/resources/web/settings/js/settings.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 4a746c8d489..e167db7513e 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -83,11 +83,18 @@ $(document).ready(function(){ for (loginIndex in formJSON["security"]["http_authentication"]) { var loginPair = formJSON["security"]["http_authentication"][loginIndex]; + var username = loginPair["http_username"].trim() || null; var password = loginPair["http_password"] || null; var passwordVerify = loginPair["http_password_verify"] || null; + if (!username) { + // Account does not have a user name, don't allow blank username. + bootbox.alert({ "message": "Account must have a username", "title": "Username Error" }); + return false; + } + if (passwordVerify && password !== passwordVerify) { - // Tried to change the password, but the password does not match + // Tried to change the password, but the password does not match. bootbox.alert({ "message": "Passwords must match!", "title": "Password Error" }); return false; } From d2495836c846464d175be98fac8950b0fa8727dc Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 11 Mar 2025 01:58:20 -0500 Subject: [PATCH 4/7] Fix accountMap value. --- domain-server/src/DomainServer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index a8d8293db31..9adb2b6abbc 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2955,8 +2955,8 @@ std::pair DomainServer::isAuthenticatedRequest(HTTPConnection* c QMap accountMap = account.toMap(); // Retrieve the values of the properties - QString httpUsername = accountMap.value("http_username_multi").toString(); - QString httpPassword = accountMap.value("http_password_multi").toString(); + QString httpUsername = accountMap.value("http_username").toString(); + QString httpPassword = accountMap.value("http_password").toString(); if (httpUsername == headerUsername) { // Found the username we are looking for From 34f41a8432b52fc5a5672a6f89524015e4b20509 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 11 Mar 2025 02:08:55 -0500 Subject: [PATCH 5/7] Prohibit duplicate usernames. --- domain-server/resources/web/settings/js/settings.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index e167db7513e..d43eaffaf38 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -80,6 +80,9 @@ $(document).ready(function(){ // Check if we have updated the basic http authentication settings if (formJSON["security"] && formJSON["security"]["http_authentication"]) { + // Keep a list of usernames while going though all accounts to see if there are any duplicates. + let usernameList = []; + for (loginIndex in formJSON["security"]["http_authentication"]) { var loginPair = formJSON["security"]["http_authentication"][loginIndex]; @@ -93,6 +96,12 @@ $(document).ready(function(){ return false; } + if (usernameList.includes(username)) { + // Account already exists with this username, don't allow duplicate usernames. + bootbox.alert({ "message": `Account already exists with the username "${username}"`, "title": "Username Error" }); + return false; + } + if (passwordVerify && password !== passwordVerify) { // Tried to change the password, but the password does not match. bootbox.alert({ "message": "Passwords must match!", "title": "Password Error" }); @@ -107,6 +116,8 @@ $(document).ready(function(){ password = sha256_digest(password); } + usernameList.push(username); + // Delete the verification field so we don't try and save it. delete formJSON["security"]["http_authentication"][loginIndex]["http_password_verify"]; From 179de44608a0873bb0bb6bda4f011ac3605b0f26 Mon Sep 17 00:00:00 2001 From: armored-dragon Date: Tue, 11 Mar 2025 02:33:22 -0500 Subject: [PATCH 6/7] Detect when attempting to change password. This will allow the user to save an updated password without poking a different setting to unlock the save button. --- domain-server/resources/web/js/base-settings.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index 1f5ff052a95..12a50c5ad87 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -311,6 +311,16 @@ $(document).ready(function(){ } }); + $('#' + Settings.FORM_ID).on('change', 'input.table-text', 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); + badgeForDifferences($(this)); + } + }); + $('#' + Settings.FORM_ID).on('change', 'select', function(){ $("input[name='" + $(this).attr('data-hidden-input') + "']").val($(this).val()).change(); }); @@ -594,7 +604,7 @@ function makeTable(setting, keypath, setting_value) { html += "" + - "" + + "" + "