diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 9cb4c2cab9..b854955953 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -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", diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index bd96f636a8..295013878c 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -2,6 +2,9 @@ var DomainInfo = null; var viewHelpers = { getFormGroup: function(keypath, setting, values, isAdvanced) { + if (setting.hidden) { + return ""; + } form_group = "
" } - - form_group += "" + setting.help + "" + if (setting.help) { + form_group += "" + setting.help + "" + } } } @@ -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); + } }); } diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index a8b7267b88..9524b18caf 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -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 "
  • " + group.label + "
  • "; diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 08d0550841..fcf7700687 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -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.
    "; + } + if (!$('input[name="oauth.provider"]').val()) { + oauthErrors += "OAuth requires a provider.
    "; + } + if (!$('input[name="oauth.hostname"]').val()) { + oauthErrors += "OAuth requires a hostname.
    "; + } + if (!$('input[name="' + SSL_PRIVATE_KEY_PATH + '"]').val() && !$('input[name="' + SSL_PRIVATE_KEY_CONTENTS_NAME + '"]').val()) { + oauthErrors += "OAuth requires an SSL Private Key.
    "; + } + if (!$('input[name="' + SSL_CERT_PATH + '"]').val() && !$('input[name="' + SSL_CERT_CONTENTS_NAME + '"]').val()) { + oauthErrors += "OAuth requires an SSL Certificate.
    "; + } + 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.
    "; + } + } + 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 = "
    "; + html += "
    "; + html += ""; + html += ""; + html += ""; + html += "
    "; + html += "
    "; + html += ""; + html += "
    Fingerprint:" + data.values.oauth["cert-fingerprint"] + "
    "; + html += ""; + html += ""; + html += ""; + html += "
    "; + + $('#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'; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 74ad014b53..7f6c366bc3 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -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->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 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 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); diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 02362abd7b..5e8eee53fe 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -236,6 +236,7 @@ private: bool _isUsingDTLS { false }; + bool _oauthEnable { false }; QUrl _oauthProviderURL; QString _oauthClientID; QString _oauthClientSecret; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 17d473f02c..2e6ccf8be2 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -22,7 +22,9 @@ #include #include #include +#include #include +#include #include #include @@ -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 > 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];