diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 5b1c3482c12..0b26f777dc6 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", @@ -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 this Domain server control panel.", + "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..a9a6cfed6fa 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(); }); @@ -473,6 +483,7 @@ function makeTable(setting, keypath, setting_value) { var html = ""; + // Apply the "help" block above the table. if (setting.help) { html += "" + setting.help + "" } @@ -480,11 +491,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 +521,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 +600,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 += "
" + 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..d43eaffaf38 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -77,30 +77,52 @@ $(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"]) { + // Keep a list of usernames while going though all accounts to see if there are any duplicates. + let usernameList = []; - // 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 = ""; - } + for (loginIndex in formJSON["security"]["http_authentication"]) { + var loginPair = formJSON["security"]["http_authentication"][loginIndex]; - // 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); + var username = loginPair["http_username"].trim() || null; + var password = loginPair["http_password"] || null; + var passwordVerify = loginPair["http_password_verify"] || null; - if (password == verify_password) { - delete formJSON["security"]["verify_http_password"]; - } else { + 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 (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" }); 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); + } + + usernameList.push(username); + + // 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 2f13fe30587..502349d24b9 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,29 @@ 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); + 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").toString(); + QString httpPassword = accountMap.value("http_password").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() ? diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index fe13c29df21..7989e3738d0 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;