Skip to content

Support multiple accounts in domain web UI #1350

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 37 additions & 21 deletions domain-server/resources/describe-settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": 2.7,
"version": 2.8,
"settings": [
{
"name": "metaverse",
Expand Down Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions domain-server/resources/web/js/base-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down Expand Up @@ -473,18 +483,21 @@ function makeTable(setting, keypath, setting_value) {

var html = "";

// Apply the "help" block above the table.
if (setting.help) {
html += "<span class='help-block'>" + setting.help + "</span>"
}

var nonDeletableRowKey = setting["non-deletable-row-key"];
var nonDeletableRowValues = setting["non-deletable-row-values"];

// Initialize the table.
html += "<table class='table table-bordered' " +
"data-short-name='" + setting.name + "' name='" + keypath + "' " +
"id='" + (!_.isUndefined(setting.html_id) ? setting.html_id : keypath) + "' " +
"data-setting-type='" + (isArray ? 'array' : 'hash') + "'>";

// Add a caption (if provided).
if (setting.caption) {
html += "<caption>" + setting.caption + "</caption>"
}
Expand All @@ -508,10 +521,12 @@ function makeTable(setting, keypath, setting_value) {
// Column names
html += "<tr class='headers'>"

// Add a number column (If enabled)
if (setting.numbered === true) {
html += "<td class='number'><strong>#</strong></td>" // Row number
}

// Add a key column (If enabled)
if (setting.key) {
html += "<td class='key'><strong>" + setting.key.label + "</strong></td>" // Key
}
Expand Down Expand Up @@ -585,6 +600,12 @@ function makeTable(setting, keypath, setting_value) {
"<input type='checkbox' class='form-control table-checkbox' " +
"name='" + colName + "'" + (colValue ? " checked" : "") + "/>" +
"</td>";
} else if (isArray && col.type === "password" && col.editable) {
html +=
"<td class='" + Settings.DATA_COL_CLASS + "' " + (col.hidden ? "style='display: none;'" : "") +
"name='" + colName + "'>" +
"<input class='form-control table-text' type='password' name='" + colName + "' value='"+ (colValue ? colValue : "") + "'/>" +
"</td>";
} else if (isArray && col.type === "time" && col.editable) {
html +=
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
Expand Down
56 changes: 39 additions & 17 deletions domain-server/resources/web/settings/js/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
31 changes: 25 additions & 6 deletions domain-server/src/DomainServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2834,8 +2834,7 @@ std::pair<bool, QString> 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.";
Expand All @@ -2856,7 +2855,7 @@ std::pair<bool, QString> 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.";
}
Expand Down Expand Up @@ -2926,7 +2925,7 @@ std::pair<bool, QString> 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";

Expand All @@ -2944,9 +2943,29 @@ std::pair<bool, QString> 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<QVariant> accountList = allAccounts.toList();

for (const QVariant &account : accountList) {
// Convert QVariant to QMap
QMap<QString, QVariant> 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() ?
Expand Down
27 changes: 27 additions & 0 deletions domain-server/src/DomainServerSettingsManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down