DEV-444 - OAuth administration improvements

This commit is contained in:
Roxanne Skelly 2019-09-10 17:25:35 -07:00
parent 51ef37fd27
commit a0ad1f3a68
7 changed files with 446 additions and 37 deletions

View file

@ -1,5 +1,5 @@
{
"version": 2.3,
"version": 2.4,
"settings": [
{
"name": "metaverse",
@ -1705,6 +1705,114 @@
}
]
},
{
"name": "oauth",
"label": "OAuth",
"show_on_enable": true,
"settings": [
{
"name": "enable",
"type": "checkbox",
"default": false,
"hidden": true
},
{
"name": "admin-users",
"label": "Admin Users",
"type": "table",
"can_add_new_rows": true,
"help": "Any of these users can administer the domain.",
"numbered": false,
"backup": false,
"advanced": false,
"columns": [
{
"name": "username",
"label": "Username",
"can_set": true
}
]
},
{
"name": "admin-roles",
"label": "Admin Roles",
"type": "table",
"can_add_new_rows": true,
"help": "Any user with any of these metaverse roles can administer the domain.",
"numbered": false,
"backup": false,
"advanced": true,
"columns": [
{
"name": "role",
"label": "Role",
"can_set": true
}
]
},
{
"name": "client-id",
"label": "Client ID",
"help": "OAuth client ID.",
"default": "",
"advanced": true,
"backup": false
},
{
"name": "client-secret",
"label": "Client Secret",
"help": "OAuth client secret.",
"type": "password",
"password_placeholder": "******",
"value-hidden": true,
"advanced": true,
"backup": false
},
{
"name": "provider",
"label": "Provider",
"help": "OAuth provider URL.",
"default": "https://metaverse.highfidelity.com",
"advanced": true,
"backup": false
},
{
"name": "hostname",
"label": "Hostname",
"help": "OAuth hostname.",
"default": "",
"advanced": true,
"backup": false
},
{
"name": "key-passphrase",
"label": "SSL Private Key Passphrase",
"help": "SSL Private Key Passphrase",
"type": "password",
"password_placeholder": "******",
"value-hidden": true,
"advanced": true,
"backup": false
},
{
"name": "cert-fingerprint",
"type": "hidden",
"readonly": true,
"advanced": true,
"backup": false
},
{
"name": "cert",
"advanced": true,
"backup": false
},
{
"name": "key",
"advanced": true,
"backup": false
}
]
},
{
"name": "automatic_content_archives",
"label": "Automatic Content Archives",

View file

@ -2,6 +2,9 @@ var DomainInfo = null;
var viewHelpers = {
getFormGroup: function(keypath, setting, values, isAdvanced) {
if (setting.hidden) {
return "";
}
form_group = "<div class='form-group " +
(isAdvanced ? Settings.ADVANCED_CLASS : "") + " " +
(setting.deprecated ? Settings.DEPRECATED_CLASS : "" ) + "' " +
@ -82,8 +85,9 @@ var viewHelpers = {
"placeholder='" + (_.has(setting, 'placeholder') ? setting.placeholder : "") +
"' value='" + (_.has(setting, 'password_placeholder') ? setting.password_placeholder : setting_value) + "'/>"
}
form_group += "<span class='help-block'>" + setting.help + "</span>"
if (setting.help) {
form_group += "<span class='help-block'>" + setting.help + "</span>"
}
}
}
@ -114,12 +118,17 @@ function reloadSettings(callback) {
data.descriptions.push(Settings.extraGroupsAtEnd[endGroupIndex]);
}
data.descriptions = data.descriptions.map(function(x) {
x.hidden = x.hidden || (x.show_on_enable && data.values[x.name] && !data.values[x.name].enable);
return x;
});
$('#panels').html(Settings.panelsTemplate(data));
Settings.data = data;
Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true);
Settings.afterReloadActions();
Settings.afterReloadActions(data);
// setup any bootstrap switches
$('.toggle-checkbox').bootstrapSwitch();
@ -129,10 +138,14 @@ function reloadSettings(callback) {
Settings.pendingChanges = 0;
// call the callback now that settings are loaded
callback(true);
if (callback) {
callback(true);
}
}).fail(function() {
// call the failure object since settings load faild
callback(false)
if (callback) {
callback(false);
}
});
}

View file

@ -91,6 +91,7 @@ $(document).ready(function(){
// make a JSON request to get the dropdown menus for content and settings
// we don't error handle here because the top level menu is still clickable and usables if this fails
$.getJSON('/settings-menu-groups.json', function(data){
function makeGroupDropdownElement(group, base) {
var html_id = group.html_id ? group.html_id : group.name;
return "<li class='setting-group'><a href='" + settingsGroupAnchor(base, html_id) + "'>" + group.label + "<span class='badge'></span></a></li>";

View file

@ -18,7 +18,19 @@ $(document).ready(function(){
Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex;
var METAVERSE_URL = URLs.METAVERSE_URL;
Settings.afterReloadActions = function() {
var SSL_PRIVATE_KEY_FILE_ID = 'ssl-private-key-file';
var SSL_PRIVATE_KEY_CONTENTS_ID = 'key-contents';
var SSL_PRIVATE_KEY_CONTENTS_NAME = 'oauth.key-contents';
var SSL_CERT_UPLOAD_ID = 'ssl-cert-button';
var SSL_CERT_FILE_ID = 'ssl-cert-file';
var SSL_CERT_FINGERPRINT_ID = 'cert-fingerprint';
var SSL_CERT_FINGERPRINT_SPAN_ID = 'cert-fingerprint-span-id';
var SSL_CERT_CONTENTS_ID = 'cert-contents';
var SSL_CERT_CONTENTS_NAME = 'oauth.cert-contents';
var SSL_PRIVATE_KEY_PATH = 'oauth.key';
var SSL_CERT_PATH = 'oauth.cert';
Settings.afterReloadActions = function(data) {
getMetaverseUrl(function(metaverse_url) {
METAVERSE_URL = metaverse_url;
@ -32,6 +44,8 @@ $(document).ready(function(){
setupDomainNetworkingSettings();
// setupDomainLabelSetting();
setupSettingsOAuth(data);
setupSettingsBackup();
if (domainIDIsSet()) {
@ -124,6 +138,48 @@ $(document).ready(function(){
}
}
if (formJSON["oauth"]) {
var private_key = formJSON["oauth"]["key-contents"];
var cert = formJSON["oauth"]["cert-contents"];
var oauthErrors = "";
if (private_key != undefined) {
var pattern = /-+BEGIN PRIVATE KEY-+[A-Za-z0-9+/\n=]*-+END PRIVATE KEY-+/m;
if (!pattern.test(private_key)) {
oauthErrors = "Private key must be in PEM format";
}
}
if (cert != undefined) {
var pattern = /-+BEGIN CERTIFICATE-+[A-Za-z0-9+/\n=]*-+END CERTIFICATE-+/m;
if (!pattern.test(cert)) {
oauthErrors = "Certificate must be in PEM format";
}
}
if ($('#oauth.panel').length) {
if (!$('input[name="oauth.client-id"]').val()) {
oauthErrors += "OAuth requires a client Id.<BR/>";
}
if (!$('input[name="oauth.provider"]').val()) {
oauthErrors += "OAuth requires a provider.<BR/>";
}
if (!$('input[name="oauth.hostname"]').val()) {
oauthErrors += "OAuth requires a hostname.<BR/>";
}
if (!$('input[name="' + SSL_PRIVATE_KEY_PATH + '"]').val() && !$('input[name="' + SSL_PRIVATE_KEY_CONTENTS_NAME + '"]').val()) {
oauthErrors += "OAuth requires an SSL Private Key.<BR/>";
}
if (!$('input[name="' + SSL_CERT_PATH + '"]').val() && !$('input[name="' + SSL_CERT_CONTENTS_NAME + '"]').val()) {
oauthErrors += "OAuth requires an SSL Certificate.<BR/>";
}
if (!$("table[name='oauth.admin-users'] tr.value-row").length &&
!$("table[name='oauth.admin-roles'] tr.value-row").length) {
oauthErrors += "OAuth must have at least one admin user or admin role.<BR/>";
}
}
if (oauthErrors) {
bootbox.alert({ "message": oauthErrors, "title": "OAuth Configuration Error" });
return false;
}
}
postSettings(formJSON);
};
@ -1035,6 +1091,67 @@ $(document).ready(function(){
});
}
function setupSettingsOAuth(data) {
// construct the HTML needed for the settings backup panel
var html = "<div class='form-group undefined'>";
html += "<label class='control-label'>SSL Private Key</label><BR/>";
html += "<label id='key-path-label'class='control-label'>Path</label>";
html += "<input id='" + SSL_PRIVATE_KEY_FILE_ID + "' type='file' accept='.key'/>";
html += "<input id='" + SSL_PRIVATE_KEY_CONTENTS_ID + "' name='" + SSL_PRIVATE_KEY_CONTENTS_NAME + "' type='hidden'/>";
html += "</div>";
html += "<div class='form-group undefined'>";
html += "<label class='control-label'>SSL Cert</label>";
html += "<div id='cert-fingerprint'><b>Fingerprint:</b><span id='" + SSL_CERT_FINGERPRINT_SPAN_ID + "'>" + data.values.oauth["cert-fingerprint"] + "</span></div>";
html += "<label id='cert-path-label' class='control-label'>Path</label>";
html += "<input id='" + SSL_CERT_FILE_ID + "' type='file' accept='.cer,.crt'/>";
html += "<input id='" + SSL_CERT_CONTENTS_ID + "' name='" + SSL_CERT_CONTENTS_NAME + "' type='hidden'/>";
html += "</div>";
$('#oauth-advanced').append(html);
$('#key-path-label').after($('[data-keypath="' + SSL_PRIVATE_KEY_PATH + '"]'));
$('#cert-path-label').after($('[data-keypath="' + SSL_CERT_PATH + '"]'));
$('[name="' + SSL_PRIVATE_KEY_PATH + '"]').val(data.values.oauth.key);
$('[name="' + SSL_CERT_PATH + '"]').val(data.values.oauth.cert);
$('body').on('change input propertychange', '#' + SSL_PRIVATE_KEY_FILE_ID, function(e){
var f = e.target.files[0];
var reader = new FileReader();
reader.onload = function(e) {
$('#' + SSL_PRIVATE_KEY_CONTENTS_ID).val(reader.result);
$('#' + SSL_PRIVATE_KEY_CONTENTS_ID).attr('data-changed', true);
$('[name="' + SSL_PRIVATE_KEY_PATH + '"]').val('');
badgeForDifferences($('#' + SSL_PRIVATE_KEY_CONTENTS_ID));
}
reader.readAsText(f);
});
$('body').on('change input propertychange', '#' + SSL_CERT_FILE_ID, function(e){
var f = e.target.files[0];
var reader = new FileReader();
reader.onload = function(e) {
$('#' + SSL_CERT_CONTENTS_ID).val(reader.result);
$('#' + SSL_CERT_CONTENTS_ID).attr('data-changed', true);
$('[name="' + SSL_CERT_PATH + '"]').val('');
$('#' + SSL_CERT_FINGERPRINT_SPAN_ID).text('');
badgeForDifferences($('#' + SSL_CERT_CONTENTS_ID));
}
reader.readAsText(f);
});
$('body').on('change input propertychange', '[name="' + SSL_PRIVATE_KEY_PATH + '"]', function(e){
$('#' + SSL_PRIVATE_KEY_FILE_ID).val('');
$('#' + SSL_PRIVATE_KEY_CONTENTS_ID).val('');
badgeForDifferences($('[name="' + SSL_PRIVATE_KEY_PATH + '"]').attr('data-changed', true));
});
$('body').on('change input propertychange', '[name="' + SSL_CERT_PATH + '"]', function(e){
$('#' + SSL_CERT_FILE_ID).val('');
$('#' + SSL_CERT_CONTENTS_ID).val('');
$('#' + SSL_CERT_FINGERPRINT_SPAN_ID).text('');
badgeForDifferences($('[name="' + SSL_CERT_PATH + '"]').attr('data-changed', true));
});
}
var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button';
var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file';

View file

@ -226,9 +226,10 @@ DomainServer::DomainServer(int argc, char* argv[]) :
setupGroupCacheRefresh();
// if we were given a certificate/private key or oauth credentials they must succeed
if (!(optionallyReadX509KeyAndCertificate() && optionallySetupOAuth())) {
return;
optionallySetupOAuth();
if (_oauthEnable) {
_oauthEnable = optionallyReadX509KeyAndCertificate();
}
_settingsManager.apiRefreshGroupInformation();
@ -447,8 +448,9 @@ QUuid DomainServer::getID() {
}
bool DomainServer::optionallyReadX509KeyAndCertificate() {
const QString X509_CERTIFICATE_OPTION = "cert";
const QString X509_PRIVATE_KEY_OPTION = "key";
const QString X509_CERTIFICATE_OPTION = "oauth.cert";
const QString X509_PRIVATE_KEY_OPTION = "oauth.key";
const QString X509_PRIVATE_KEY_PASSPHRASE_OPTION = "oauth.key-passphrase";
const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE";
QString certPath = _settingsManager.valueForKeyPath(X509_CERTIFICATE_OPTION).toString();
@ -459,7 +461,12 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() {
// this is used for Oauth callbacks when authorizing users against a data server
// let's make sure we can load the key and certificate
QString keyPassphraseString = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV);
QString keyPassphraseEnv = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV);
QString keyPassphraseString = _settingsManager.valueForKeyPath(X509_PRIVATE_KEY_PASSPHRASE_OPTION).toString();
if (!keyPassphraseEnv.isEmpty()) {
keyPassphraseString = keyPassphraseEnv;
}
qDebug() << "Reading certificate file at" << certPath << "for HTTPS.";
qDebug() << "Reading key file at" << keyPath << "for HTTPS.";
@ -473,16 +480,15 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() {
QSslCertificate sslCertificate(&certFile);
QSslKey privateKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8());
if (privateKey.isNull()) {
qCritical() << "SSL Private Key Not Loading. Bad password or key format?";
}
_httpsManager.reset(new HTTPSManager(QHostAddress::AnyIPv4, DOMAIN_SERVER_HTTPS_PORT, sslCertificate, privateKey, QString(), this));
qDebug() << "TCP server listening for HTTPS connections on" << DOMAIN_SERVER_HTTPS_PORT;
} else if (!certPath.isEmpty() || !keyPath.isEmpty()) {
static const QString MISSING_CERT_ERROR_MSG = "Missing certificate or private key. domain-server will now quit.";
static const int MISSING_CERT_ERROR_CODE = 3;
QMetaObject::invokeMethod(this, "queuedQuit", Qt::QueuedConnection,
Q_ARG(QString, MISSING_CERT_ERROR_MSG), Q_ARG(int, MISSING_CERT_ERROR_CODE));
return false;
}
@ -490,10 +496,12 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() {
}
bool DomainServer::optionallySetupOAuth() {
const QString OAUTH_PROVIDER_URL_OPTION = "oauth-provider";
const QString OAUTH_CLIENT_ID_OPTION = "oauth-client-id";
const QString OAUTH_ENABLE_OPTION = "oauth.enable";
const QString OAUTH_PROVIDER_URL_OPTION = "oauth.provider";
const QString OAUTH_CLIENT_ID_OPTION = "oauth.client-id";
const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET";
const QString REDIRECT_HOSTNAME_OPTION = "hostname";
const QString OAUTH_CLIENT_SECRET_OPTION = "oauth.client-secret";
const QString REDIRECT_HOSTNAME_OPTION = "oauth.hostname";
_oauthProviderURL = QUrl(_settingsManager.valueForKeyPath(OAUTH_PROVIDER_URL_OPTION).toString());
@ -502,22 +510,24 @@ bool DomainServer::optionallySetupOAuth() {
_oauthProviderURL = NetworkingConstants::METAVERSE_SERVER_URL();
}
_oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV);
if (_oauthClientSecret.isEmpty()) {
_oauthClientSecret = _settingsManager.valueForKeyPath(OAUTH_CLIENT_SECRET_OPTION).toString();
}
auto accountManager = DependencyManager::get<AccountManager>();
accountManager->setAuthURL(_oauthProviderURL);
_oauthClientID = _settingsManager.valueForKeyPath(OAUTH_CLIENT_ID_OPTION).toString();
_oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV);
_hostname = _settingsManager.valueForKeyPath(REDIRECT_HOSTNAME_OPTION).toString();
if (!_oauthClientID.isEmpty()) {
_oauthEnable = _settingsManager.valueForKeyPath(OAUTH_ENABLE_OPTION).toBool();
if (_oauthEnable) {
if (_oauthProviderURL.isEmpty()
|| _hostname.isEmpty()
|| _oauthClientID.isEmpty()
|| _oauthClientSecret.isEmpty()) {
static const QString MISSING_OAUTH_INFO_MSG = "Missing OAuth provider URL, hostname, client ID, or client secret. domain-server will now quit.";
static const int MISSING_OAUTH_INFO_ERROR_CODE = 4;
QMetaObject::invokeMethod(this, "queuedQuit", Qt::QueuedConnection,
Q_ARG(QString, MISSING_OAUTH_INFO_MSG), Q_ARG(int, MISSING_OAUTH_INFO_ERROR_CODE));
_oauthEnable = false;
return false;
} else {
qDebug() << "OAuth will be used to identify clients using provider at" << _oauthProviderURL.toString();
@ -2693,8 +2703,8 @@ void DomainServer::profileRequestFinished() {
std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* connection) {
static const QByteArray HTTP_COOKIE_HEADER_KEY = "Cookie";
static const QString ADMIN_USERS_CONFIG_KEY = "admin-users";
static const QString ADMIN_ROLES_CONFIG_KEY = "admin-roles";
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";
const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)";
@ -2704,12 +2714,11 @@ std::pair<bool, QString> DomainServer::isAuthenticatedRequest(HTTPConnection* c
QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY);
QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY);
if (!_oauthProviderURL.isEmpty()
&& (adminUsersVariant.isValid() || adminRolesVariant.isValid())) {
if (_oauthEnable) {
QString cookieString = connection->requestHeader(HTTP_COOKIE_HEADER_KEY);
QRegExp cookieUUIDRegex(COOKIE_UUID_REGEX_STRING);
QUuid cookieUUID;
if (cookieString.indexOf(cookieUUIDRegex) != -1) {
cookieUUID = cookieUUIDRegex.cap(1);

View file

@ -236,6 +236,7 @@ private:
bool _isUsingDTLS { false };
bool _oauthEnable { false };
QUrl _oauthProviderURL;
QString _oauthClientID;
QString _oauthClientSecret;

View file

@ -22,7 +22,9 @@
#include <QtCore/QThread>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <QtNetwork/QSslKey>
#include <QSaveFile>
#include <QPair>
#include <AccountManager.h>
#include <Assignment.h>
@ -46,10 +48,14 @@ const QString DESCRIPTION_SETTINGS_KEY = "settings";
const QString SETTING_DEFAULT_KEY = "default";
const QString DESCRIPTION_NAME_KEY = "name";
const QString DESCRIPTION_GROUP_LABEL_KEY = "label";
const QString DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY = "show_on_enable";
const QString DESCRIPTION_ENABLE_KEY = "enable";
const QString DESCRIPTION_BACKUP_FLAG_KEY = "backup";
const QString SETTING_DESCRIPTION_TYPE_KEY = "type";
const QString DESCRIPTION_COLUMNS_KEY = "columns";
const QString CONTENT_SETTING_FLAG_KEY = "content_setting";
static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings";
static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings";
const QString SETTINGS_VIEWPOINT_KEY = "viewpoint";
@ -136,6 +142,10 @@ void DomainServerSettingsManager::splitSettingsDescription() {
settingsDropdownGroup[DESCRIPTION_GROUP_LABEL_KEY] = groupObject[DESCRIPTION_GROUP_LABEL_KEY];
if (groupObject.contains(DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY)) {
settingsDropdownGroup[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY] = groupObject[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY];
}
static const QString DESCRIPTION_GROUP_HTML_ID_KEY = "html_id";
if (groupObject.contains(DESCRIPTION_GROUP_HTML_ID_KEY)) {
settingsDropdownGroup[DESCRIPTION_GROUP_HTML_ID_KEY] = groupObject[DESCRIPTION_GROUP_HTML_ID_KEY];
@ -170,9 +180,6 @@ void DomainServerSettingsManager::splitSettingsDescription() {
// populate the settings menu groups with what we've collected
static const QString SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY = "domain_settings";
static const QString SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY = "content_settings";
_settingsMenuGroups[SPLIT_MENU_GROUPS_DOMAIN_SETTINGS_KEY] = domainSettingsMenuGroups;
_settingsMenuGroups[SPLIT_MENU_GROUPS_CONTENT_SETTINGS_KEY] = contentSettingsMenuGroups;
}
@ -448,6 +455,77 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena
packPermissions();
}
if (oldVersion < 2.4) {
// migrate oauth settings to their own group
const QString ADMIN_USERS = "admin-users";
const QString OAUTH_ADMIN_USERS = "oauth.admin-users";
const QString OAUTH_CLIENT_ID = "oauth.client-id";
const QString ALT_ADMIN_USERS = "admin.users";
const QString ADMIN_ROLES = "admin-roles";
const QString OAUTH_ADMIN_ROLES = "oauth.admin-roles";
const QString OAUTH_ENABLE = "oauth.enable";
QVector<QPair<const char*, const char*> > conversionMap = {
{"key", "oauth.key"},
{"cert", "oauth.cert"},
{"hostname", "oauth.hostname"},
{"oauth-client-id", "oauth.client-id"},
{"oauth-provider", "oauth.provider"}
};
for (auto & conversion : conversionMap) {
QVariant* prevValue = _configMap.valueForKeyPath(conversion.first);
if (prevValue) {
auto newValue = _configMap.valueForKeyPath(conversion.second, true);
*newValue = *prevValue;
}
}
QVariant* client_id = _configMap.valueForKeyPath(OAUTH_CLIENT_ID);
if (client_id) {
QVariant* oauthEnable = _configMap.valueForKeyPath(OAUTH_ENABLE, true);
*oauthEnable = QVariant(true);
}
QVariant* oldAdminUsers = _configMap.valueForKeyPath(ADMIN_USERS);
QVariant* newAdminUsers = _configMap.valueForKeyPath(OAUTH_ADMIN_USERS, true);
QVariantList adminUsers(newAdminUsers->toList());
if (oldAdminUsers) {
QStringList adminUsersList = oldAdminUsers->toStringList();
for (auto & user : adminUsersList) {
if (!adminUsers.contains(user)) {
adminUsers.append(user);
}
}
}
QVariant* altAdminUsers = _configMap.valueForKeyPath(ALT_ADMIN_USERS);
if (altAdminUsers) {
QStringList adminUsersList = altAdminUsers->toStringList();
for (auto & user : adminUsersList) {
if (!adminUsers.contains(user)) {
adminUsers.append(user);
}
}
}
*newAdminUsers = adminUsers;
QVariant* oldAdminRoles = _configMap.valueForKeyPath(ADMIN_ROLES);
QVariant* newAdminRoles = _configMap.valueForKeyPath(OAUTH_ADMIN_ROLES, true);
QVariantList adminRoles(newAdminRoles->toList());
if (oldAdminRoles) {
QStringList adminRoleList = oldAdminRoles->toStringList();
for (auto & role : adminRoleList) {
if (!adminRoles.contains(role)) {
adminRoles.append(role);
}
}
}
*newAdminRoles = adminRoles;
}
// write the current description version to our settings
*versionVariant = _descriptionVersion;
@ -1185,7 +1263,23 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
return true;
} else if (url.path() == SETTINGS_MENU_GROUPS_PATH) {
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(_settingsMenuGroups).toJson(), "application/json");
QJsonObject settings;
for (auto & key : _settingsMenuGroups.keys()) {
const QJsonArray& settingGroups = _settingsMenuGroups[key].toArray();
QJsonArray groups;
foreach (const QJsonValue& group, settingGroups) {
QJsonObject groupObject = group.toObject();
if (!groupObject.contains(DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY)
|| (groupObject[DESCRIPTION_GROUP_SHOW_ON_ENABLE_KEY].toBool()
&& _configMap.valueForKeyPath(groupObject[DESCRIPTION_NAME_KEY].toString() + "." + DESCRIPTION_ENABLE_KEY)->toBool() )) {
groups.append(groupObject);
}
}
settings[key] = groups;
}
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(settings).toJson(), "application/json");
return true;
} else if (url.path() == SETTINGS_BACKUP_PATH) {
@ -1440,12 +1534,35 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt
}
if (!groupKey.isEmpty() && !groupResponseObject.isEmpty()) {
// set this group's object to the constructed object
responseObject[groupKey] = groupResponseObject;
}
}
}
// add 'derived' values used primarily for UI
const QString X509_CERTIFICATE_OPTION = "oauth.cert";
QString certPath = valueForKeyPath(X509_CERTIFICATE_OPTION).toString();
if (!certPath.isEmpty()) {
// the user wants to use the following cert and key for HTTPS
// this is used for Oauth callbacks when authorizing users against a data server
// let's make sure we can load the key and certificate
qDebug() << "Reading certificate file at" << certPath << "for HTTPS.";
QFile certFile(certPath);
certFile.open(QIODevice::ReadOnly);
QSslCertificate sslCertificate(&certFile);
QString digest = sslCertificate.digest().toHex(':');
auto groupObject = responseObject["oauth"].toObject();
groupObject["cert-fingerprint"] = digest;
responseObject["oauth"] = groupObject;
}
return responseObject;
}
@ -1551,23 +1668,66 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
return QJsonObject();
}
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject,
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedSettingsObject,
SettingsType settingsType) {
// take a write lock since we're about to overwrite settings in the config map
QWriteLocker locker(&_settingsLock);
QJsonObject postedObject(postedSettingsObject);
static const QString SECURITY_ROOT_KEY = "security";
static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist";
static const QString BROADCASTING_KEY = "broadcasting";
static const QString WIZARD_KEY = "wizard";
static const QString DESCRIPTION_ROOT_KEY = "descriptors";
static const QString OAUTH_ROOT_KEY = "oauth";
static const QString OAUTH_KEY_CONTENTS = "key-contents";
static const QString OAUTH_CERT_CONTENTS = "cert-contents";
static const QString OAUTH_CERT_PATH = "cert";
static const QString OAUTH_KEY_PASSPHRASE = "key-passphrase";
static const QString OAUTH_KEY_PATH = "key";
auto& settingsVariant = _configMap.getConfig();
bool needRestart = false;
auto& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription;
auto oauthObject = postedObject[OAUTH_ROOT_KEY].toObject();
if (oauthObject.contains(OAUTH_CERT_CONTENTS)) {
QSslCertificate cert(oauthObject[OAUTH_CERT_CONTENTS].toString().toUtf8());
if (!cert.isNull()) {
static const QString CERT_FILE_NAME = "certificate.crt";
auto certPath = PathUtils::getAppDataFilePath(CERT_FILE_NAME);
QFile file(certPath);
if (file.open(QFile::WriteOnly)) {
file.write(cert.toPem());
file.close();
}
oauthObject[OAUTH_CERT_PATH] = certPath;
}
oauthObject.remove(OAUTH_CERT_CONTENTS);
}
if (oauthObject.contains(OAUTH_KEY_CONTENTS)) {
QString keyPassphraseString = oauthObject[OAUTH_KEY_PASSPHRASE].toString();
QSslKey key(oauthObject[OAUTH_KEY_CONTENTS].toString().toUtf8(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, keyPassphraseString.toUtf8());
if (!key.isNull()) {
static const QString KEY_FILE_NAME = "certificate.key";
auto keyPath = PathUtils::getAppDataFilePath(KEY_FILE_NAME);
QFile file(keyPath);
if (file.open(QFile::WriteOnly)) {
file.write(key.toPem());
file.close();
file.setPermissions(QFile::ReadOwner | QFile::WriteOwner);
}
oauthObject[OAUTH_KEY_PATH] = keyPath;
}
oauthObject.remove(OAUTH_KEY_CONTENTS);
}
postedObject[OAUTH_ROOT_KEY] = oauthObject;
qDebug() << postedObject;
// Iterate on the setting groups
foreach(const QString& rootKey, postedObject.keys()) {
const QJsonValue& rootValue = postedObject[rootKey];