mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-26 03:15:21 +02:00
2054 lines
91 KiB
C++
2054 lines
91 KiB
C++
//
|
|
// DomainServerSettingsManager.cpp
|
|
// domain-server/src
|
|
//
|
|
// Created by Stephen Birarda on 2014-06-24.
|
|
// Copyright 2014 High Fidelity, Inc.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
#include "DomainServerSettingsManager.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <QtCore/QCoreApplication>
|
|
#include <QtCore/QDir>
|
|
#include <QtCore/QFile>
|
|
#include <QtCore/QJsonArray>
|
|
#include <QtCore/QJsonObject>
|
|
#include <QtCore/QStandardPaths>
|
|
#include <QtCore/QThread>
|
|
#include <QtCore/QUrl>
|
|
#include <QtCore/QUrlQuery>
|
|
|
|
#include <AccountManager.h>
|
|
#include <Assignment.h>
|
|
#include <HifiConfigVariantMap.h>
|
|
#include <HTTPConnection.h>
|
|
#include <NLPacketList.h>
|
|
#include <NumericalConstants.h>
|
|
#include <SettingHandle.h>
|
|
#include <SettingHelpers.h>
|
|
#include <AvatarData.h> //for KillAvatarReason
|
|
#include <FingerprintUtils.h>
|
|
#include <shared/QtHelpers.h>
|
|
|
|
#include "DomainServerNodeData.h"
|
|
|
|
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
|
|
const QString SETTINGS_PATH = "/settings";
|
|
const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
|
|
const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json";
|
|
|
|
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_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";
|
|
|
|
const QString SETTINGS_VIEWPOINT_KEY = "viewpoint";
|
|
|
|
DomainServerSettingsManager::DomainServerSettingsManager() {
|
|
// load the description object from the settings description
|
|
QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH);
|
|
descriptionFile.open(QIODevice::ReadOnly);
|
|
|
|
QJsonParseError parseError;
|
|
QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll(), &parseError);
|
|
|
|
if (descriptionDocument.isObject()) {
|
|
QJsonObject descriptionObject = descriptionDocument.object();
|
|
|
|
const QString DESCRIPTION_VERSION_KEY = "version";
|
|
|
|
if (descriptionObject.contains(DESCRIPTION_VERSION_KEY)) {
|
|
// read the version from the settings description
|
|
_descriptionVersion = descriptionObject[DESCRIPTION_VERSION_KEY].toDouble();
|
|
|
|
if (descriptionObject.contains(DESCRIPTION_SETTINGS_KEY)) {
|
|
_descriptionArray = descriptionDocument.object()[DESCRIPTION_SETTINGS_KEY].toArray();
|
|
splitSettingsDescription();
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static const QString MISSING_SETTINGS_DESC_MSG =
|
|
QString("Did not find settings description in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3")
|
|
.arg(SETTINGS_DESCRIPTION_RELATIVE_PATH).arg(parseError.errorString()).arg(parseError.offset);
|
|
static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6;
|
|
|
|
QMetaObject::invokeMethod(QCoreApplication::instance(), "queuedQuit", Qt::QueuedConnection,
|
|
Q_ARG(QString, MISSING_SETTINGS_DESC_MSG),
|
|
Q_ARG(int, MISSING_SETTINGS_DESC_ERROR_CODE));
|
|
}
|
|
|
|
void DomainServerSettingsManager::splitSettingsDescription() {
|
|
// construct separate description arrays for domain settings and content settings
|
|
// since they are displayed on different pages
|
|
|
|
// along the way we also construct one object that holds the groups separated by domain settings
|
|
// and content settings, so that the DS can setup dropdown menus below "Content" and "Settings"
|
|
// headers to jump directly to a settings group on the page of either
|
|
QJsonArray domainSettingsMenuGroups;
|
|
QJsonArray contentSettingsMenuGroups;
|
|
|
|
foreach(const QJsonValue& group, _descriptionArray) {
|
|
QJsonObject groupObject = group.toObject();
|
|
|
|
static const QString HIDDEN_GROUP_KEY = "hidden";
|
|
bool groupHidden = groupObject.contains(HIDDEN_GROUP_KEY) && groupObject[HIDDEN_GROUP_KEY].toBool();
|
|
|
|
QJsonArray domainSettingArray;
|
|
QJsonArray contentSettingArray;
|
|
|
|
foreach(const QJsonValue& settingDescription, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) {
|
|
QJsonObject settingDescriptionObject = settingDescription.toObject();
|
|
|
|
bool isContentSetting = settingDescriptionObject.contains(CONTENT_SETTING_FLAG_KEY)
|
|
&& settingDescriptionObject[CONTENT_SETTING_FLAG_KEY].toBool();
|
|
|
|
if (isContentSetting) {
|
|
// push the setting description to the pending content setting array
|
|
contentSettingArray.push_back(settingDescriptionObject);
|
|
} else {
|
|
// push the setting description to the pending domain setting array
|
|
domainSettingArray.push_back(settingDescriptionObject);
|
|
}
|
|
}
|
|
|
|
if (!domainSettingArray.isEmpty() || !contentSettingArray.isEmpty()) {
|
|
|
|
// we know for sure we'll have something to add to our settings menu groups
|
|
// so setup that object for the group now, as long as the group isn't hidden alltogether
|
|
QJsonObject settingsDropdownGroup;
|
|
|
|
if (!groupHidden) {
|
|
if (groupObject.contains(DESCRIPTION_NAME_KEY)) {
|
|
settingsDropdownGroup[DESCRIPTION_NAME_KEY] = groupObject[DESCRIPTION_NAME_KEY];
|
|
}
|
|
|
|
settingsDropdownGroup[DESCRIPTION_GROUP_LABEL_KEY] = groupObject[DESCRIPTION_GROUP_LABEL_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];
|
|
}
|
|
}
|
|
|
|
if (!domainSettingArray.isEmpty()) {
|
|
// we have some domain settings from this group, add the group with the filtered settings
|
|
QJsonObject filteredGroupObject = groupObject;
|
|
filteredGroupObject[DESCRIPTION_SETTINGS_KEY] = domainSettingArray;
|
|
_domainSettingsDescription.push_back(filteredGroupObject);
|
|
|
|
// if the group isn't hidden, add its information to the domain settings menu groups
|
|
if (!groupHidden) {
|
|
domainSettingsMenuGroups.push_back(settingsDropdownGroup);
|
|
}
|
|
}
|
|
|
|
if (!contentSettingArray.isEmpty()) {
|
|
// we have some content settings from this group, add the group with the filtered settings
|
|
QJsonObject filteredGroupObject = groupObject;
|
|
filteredGroupObject[DESCRIPTION_SETTINGS_KEY] = contentSettingArray;
|
|
_contentSettingsDescription.push_back(filteredGroupObject);
|
|
|
|
// if the group isn't hidden, add its information to the content settings menu groups
|
|
if (!groupHidden) {
|
|
contentSettingsMenuGroups.push_back(settingsDropdownGroup);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer<ReceivedMessage> message) {
|
|
Assignment::Type type;
|
|
message->readPrimitive(&type);
|
|
|
|
QJsonObject responseObject = settingsResponseObjectForType(QString::number(type));
|
|
auto json = QJsonDocument(responseObject).toJson();
|
|
|
|
auto packetList = NLPacketList::create(PacketType::DomainSettings, QByteArray(), true, true);
|
|
|
|
packetList->write(json);
|
|
|
|
auto nodeList = DependencyManager::get<LimitedNodeList>();
|
|
nodeList->sendPacketList(std::move(packetList), message->getSenderSockAddr());
|
|
}
|
|
|
|
void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList) {
|
|
// since we're called from the DomainServerSettingsManager constructor, we don't take a write lock here
|
|
// even though we change the underlying config map
|
|
|
|
_argumentList = argumentList;
|
|
|
|
_configMap.loadConfig(_argumentList);
|
|
|
|
static const auto VERSION_SETTINGS_KEYPATH = "version";
|
|
QVariant* versionVariant = _configMap.valueForKeyPath(VERSION_SETTINGS_KEYPATH);
|
|
|
|
if (!versionVariant) {
|
|
versionVariant = _configMap.valueForKeyPath(VERSION_SETTINGS_KEYPATH, true);
|
|
*versionVariant = _descriptionVersion;
|
|
persistToFile();
|
|
qDebug() << "No version in config file, setting to current version" << _descriptionVersion;
|
|
}
|
|
|
|
{
|
|
// Backward compatibility migration code
|
|
// The config version used to be stored in a different file
|
|
// This moves it to the actual config file.
|
|
Setting::Handle<double> JSON_SETTING_VERSION("json-settings/version", 0.0);
|
|
if (JSON_SETTING_VERSION.isSet()) {
|
|
auto version = JSON_SETTING_VERSION.get();
|
|
*versionVariant = version;
|
|
persistToFile();
|
|
QFile::remove(settingsFilename());
|
|
}
|
|
}
|
|
|
|
// What settings version were we before and what are we using now?
|
|
// Do we need to do any re-mapping?
|
|
double oldVersion = versionVariant->toDouble();
|
|
|
|
if (oldVersion != _descriptionVersion) {
|
|
const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
|
|
const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access";
|
|
const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors";
|
|
const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers";
|
|
const QString EDITORS_CAN_REPLACE_CONTENT_KEYPATH = "security.editors_can_replace_content";
|
|
|
|
qDebug() << "Previous domain-server settings version was"
|
|
<< QString::number(oldVersion, 'g', 8) << "and the new version is"
|
|
<< QString::number(_descriptionVersion, 'g', 8) << "- checking if any re-mapping is required";
|
|
|
|
// we have a version mismatch - for now handle custom behaviour here since there are not many remappings
|
|
if (oldVersion < 1.0) {
|
|
// This was prior to the introduction of security.restricted_access
|
|
// If the user has a list of allowed users then set their value for security.restricted_access to true
|
|
|
|
QVariant* allowedUsers = _configMap.valueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH);
|
|
|
|
if (allowedUsers
|
|
&& allowedUsers->canConvert(QMetaType::QVariantList)
|
|
&& reinterpret_cast<QVariantList*>(allowedUsers)->size() > 0) {
|
|
|
|
qDebug() << "Forcing security.restricted_access to TRUE since there was an"
|
|
<< "existing list of allowed users.";
|
|
|
|
// In the pre-toggle system the user had a list of allowed users, so
|
|
// we need to set security.restricted_access to true
|
|
QVariant* restrictedAccess = _configMap.valueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH, true);
|
|
|
|
*restrictedAccess = QVariant(true);
|
|
}
|
|
}
|
|
|
|
if (oldVersion < 1.1) {
|
|
static const QString ENTITY_SERVER_SETTINGS_KEY = "entity_server_settings";
|
|
static const QString ENTITY_FILE_NAME_KEY = "persistFilename";
|
|
static const QString ENTITY_FILE_PATH_KEYPATH = ENTITY_SERVER_SETTINGS_KEY + ".persistFilePath";
|
|
|
|
// this was prior to change of poorly named entitiesFileName to entitiesFilePath
|
|
QVariant* persistFileNameVariant = _configMap.valueForKeyPath(ENTITY_SERVER_SETTINGS_KEY + "." + ENTITY_FILE_NAME_KEY);
|
|
if (persistFileNameVariant && persistFileNameVariant->canConvert(QMetaType::QString)) {
|
|
QString persistFileName = persistFileNameVariant->toString();
|
|
|
|
qDebug() << "Migrating persistFilename to persistFilePath for entity-server settings";
|
|
|
|
// grab the persistFilePath option, create it if it doesn't exist
|
|
QVariant* persistFilePath = _configMap.valueForKeyPath(ENTITY_FILE_PATH_KEYPATH, true);
|
|
|
|
// write the migrated value
|
|
*persistFilePath = persistFileName;
|
|
|
|
// remove the old setting
|
|
QVariant* entityServerVariant = _configMap.valueForKeyPath(ENTITY_SERVER_SETTINGS_KEY);
|
|
if (entityServerVariant && entityServerVariant->canConvert(QMetaType::QVariantMap)) {
|
|
QVariantMap entityServerMap = entityServerVariant->toMap();
|
|
entityServerMap.remove(ENTITY_FILE_NAME_KEY);
|
|
|
|
*entityServerVariant = entityServerMap;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (oldVersion < 1.2) {
|
|
// This was prior to the base64 encoding of password for HTTP Basic Authentication.
|
|
// If we have a password in the previous settings file, make it base 64
|
|
static const QString BASIC_AUTH_PASSWORD_KEY_PATH { "security.http_password" };
|
|
|
|
QVariant* passwordVariant = _configMap.valueForKeyPath(BASIC_AUTH_PASSWORD_KEY_PATH);
|
|
|
|
if (passwordVariant && passwordVariant->canConvert(QMetaType::QString)) {
|
|
QString plaintextPassword = passwordVariant->toString();
|
|
|
|
qDebug() << "Migrating plaintext password to SHA256 hash in domain-server settings.";
|
|
|
|
*passwordVariant = QCryptographicHash::hash(plaintextPassword.toUtf8(), QCryptographicHash::Sha256).toHex();
|
|
}
|
|
}
|
|
|
|
if (oldVersion < 1.4) {
|
|
// This was prior to the permissions-grid in the domain-server settings page
|
|
bool isRestrictedAccess = valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
|
|
QStringList allowedUsers = valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList();
|
|
QStringList allowedEditors = valueOrDefaultValueForKeyPath(ALLOWED_EDITORS_SETTINGS_KEYPATH).toStringList();
|
|
bool onlyEditorsAreRezzers = valueOrDefaultValueForKeyPath(EDITORS_ARE_REZZERS_KEYPATH).toBool();
|
|
|
|
_standardAgentPermissions[NodePermissions::standardNameLocalhost].reset(
|
|
new NodePermissions(NodePermissions::standardNameLocalhost));
|
|
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->setAll(true);
|
|
_standardAgentPermissions[NodePermissions::standardNameAnonymous].reset(
|
|
new NodePermissions(NodePermissions::standardNameAnonymous));
|
|
_standardAgentPermissions[NodePermissions::standardNameLoggedIn].reset(
|
|
new NodePermissions(NodePermissions::standardNameLoggedIn));
|
|
_standardAgentPermissions[NodePermissions::standardNameFriends].reset(
|
|
new NodePermissions(NodePermissions::standardNameFriends));
|
|
|
|
if (isRestrictedAccess) {
|
|
// only users in allow-users list can connect
|
|
_standardAgentPermissions[NodePermissions::standardNameAnonymous]->clear(
|
|
NodePermissions::Permission::canConnectToDomain);
|
|
_standardAgentPermissions[NodePermissions::standardNameLoggedIn]->clear(
|
|
NodePermissions::Permission::canConnectToDomain);
|
|
} // else anonymous and logged-in retain default of canConnectToDomain = true
|
|
|
|
foreach (QString allowedUser, allowedUsers) {
|
|
// even if isRestrictedAccess is false, we have to add explicit rows for these users.
|
|
_agentPermissions[NodePermissionsKey(allowedUser, 0)].reset(new NodePermissions(allowedUser));
|
|
_agentPermissions[NodePermissionsKey(allowedUser, 0)]->set(NodePermissions::Permission::canConnectToDomain);
|
|
}
|
|
|
|
foreach (QString allowedEditor, allowedEditors) {
|
|
NodePermissionsKey editorKey(allowedEditor, 0);
|
|
if (!_agentPermissions.contains(editorKey)) {
|
|
_agentPermissions[editorKey].reset(new NodePermissions(allowedEditor));
|
|
if (isRestrictedAccess) {
|
|
// they can change locks, but can't connect.
|
|
_agentPermissions[editorKey]->clear(NodePermissions::Permission::canConnectToDomain);
|
|
}
|
|
}
|
|
_agentPermissions[editorKey]->set(NodePermissions::Permission::canAdjustLocks);
|
|
}
|
|
|
|
std::list<std::unordered_map<NodePermissionsKey, NodePermissionsPointer>> permissionsSets{
|
|
_standardAgentPermissions.get(),
|
|
_agentPermissions.get()
|
|
};
|
|
foreach (auto permissionsSet, permissionsSets) {
|
|
for (auto entry : permissionsSet) {
|
|
const auto& userKey = entry.first;
|
|
if (onlyEditorsAreRezzers) {
|
|
if (permissionsSet[userKey]->can(NodePermissions::Permission::canAdjustLocks)) {
|
|
permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities);
|
|
permissionsSet[userKey]->set(NodePermissions::Permission::canRezTemporaryEntities);
|
|
} else {
|
|
permissionsSet[userKey]->clear(NodePermissions::Permission::canRezPermanentEntities);
|
|
permissionsSet[userKey]->clear(NodePermissions::Permission::canRezTemporaryEntities);
|
|
}
|
|
} else {
|
|
permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities);
|
|
permissionsSet[userKey]->set(NodePermissions::Permission::canRezTemporaryEntities);
|
|
}
|
|
}
|
|
}
|
|
|
|
packPermissions();
|
|
_standardAgentPermissions.clear();
|
|
_agentPermissions.clear();
|
|
}
|
|
|
|
if (oldVersion < 1.6) {
|
|
unpackPermissions();
|
|
|
|
// This was prior to addition of kick permissions, add that to localhost permissions by default
|
|
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canKick);
|
|
|
|
packPermissions();
|
|
}
|
|
|
|
if (oldVersion < 1.8) {
|
|
unpackPermissions();
|
|
// This was prior to addition of domain content replacement, add that to localhost permissions by default
|
|
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canReplaceDomainContent);
|
|
packPermissions();
|
|
}
|
|
|
|
if (oldVersion < 1.9) {
|
|
unpackPermissions();
|
|
// This was prior to addition of canRez(Tmp)Certified; add those to localhost permissions by default
|
|
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canRezPermanentCertifiedEntities);
|
|
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities);
|
|
packPermissions();
|
|
}
|
|
|
|
if (oldVersion < 2.0) {
|
|
const QString WIZARD_COMPLETED_ONCE = "wizard.completed_once";
|
|
|
|
QVariant* wizardCompletedOnce = _configMap.valueForKeyPath(WIZARD_COMPLETED_ONCE, true);
|
|
|
|
*wizardCompletedOnce = QVariant(true);
|
|
}
|
|
|
|
if (oldVersion < 2.1) {
|
|
// convert old avatar scale settings into avatar height.
|
|
|
|
const QString AVATAR_MIN_SCALE_KEYPATH = "avatars.min_avatar_scale";
|
|
const QString AVATAR_MAX_SCALE_KEYPATH = "avatars.max_avatar_scale";
|
|
const QString AVATAR_MIN_HEIGHT_KEYPATH = "avatars.min_avatar_height";
|
|
const QString AVATAR_MAX_HEIGHT_KEYPATH = "avatars.max_avatar_height";
|
|
|
|
QVariant* avatarMinScale = _configMap.valueForKeyPath(AVATAR_MIN_SCALE_KEYPATH);
|
|
if (avatarMinScale) {
|
|
auto newMinScaleVariant = _configMap.valueForKeyPath(AVATAR_MIN_HEIGHT_KEYPATH, true);
|
|
*newMinScaleVariant = avatarMinScale->toFloat() * DEFAULT_AVATAR_HEIGHT;
|
|
}
|
|
|
|
QVariant* avatarMaxScale = _configMap.valueForKeyPath(AVATAR_MAX_SCALE_KEYPATH);
|
|
if (avatarMaxScale) {
|
|
auto newMaxScaleVariant = _configMap.valueForKeyPath(AVATAR_MAX_HEIGHT_KEYPATH, true);
|
|
*newMaxScaleVariant = avatarMaxScale->toFloat() * DEFAULT_AVATAR_HEIGHT;
|
|
}
|
|
}
|
|
|
|
if (oldVersion < 2.2) {
|
|
// migrate entity server rolling backup intervals to new location for automatic content archive intervals
|
|
|
|
const QString ENTITY_SERVER_BACKUPS_KEYPATH = "entity_server_settings.backups";
|
|
const QString AUTO_CONTENT_ARCHIVES_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules";
|
|
|
|
QVariant* previousBackupsVariant = _configMap.valueForKeyPath(ENTITY_SERVER_BACKUPS_KEYPATH);
|
|
|
|
if (previousBackupsVariant) {
|
|
auto migratedBackupsVariant = _configMap.valueForKeyPath(AUTO_CONTENT_ARCHIVES_RULES_KEYPATH, true);
|
|
*migratedBackupsVariant = *previousBackupsVariant;
|
|
}
|
|
}
|
|
|
|
|
|
// write the current description version to our settings
|
|
*versionVariant = _descriptionVersion;
|
|
|
|
// write the new settings to the json file
|
|
persistToFile();
|
|
}
|
|
|
|
unpackPermissions();
|
|
}
|
|
|
|
void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& permissionsRows,
|
|
QString groupName, NodePermissionsPointer perms) {
|
|
// this is called when someone has used the domain-settings webpage to add a group. They type the group's name
|
|
// and give it some permissions. The domain-server asks api for the group's ranks and populates the map
|
|
// with them. Here, that initial user-entered row is removed and it's permissions are copied to all the ranks
|
|
// except owner.
|
|
|
|
QString groupNameLower = groupName.toLower();
|
|
|
|
foreach (NodePermissionsKey nameKey, permissionsRows.keys()) {
|
|
if (nameKey.first.toLower() != groupNameLower) {
|
|
continue;
|
|
}
|
|
QUuid groupID = _groupIDs[groupNameLower.toLower()];
|
|
QUuid rankID = nameKey.second;
|
|
GroupRank rank = _groupRanks[groupID][rankID];
|
|
if (rank.order == 0) {
|
|
// we don't copy the initial permissions to the owner.
|
|
continue;
|
|
}
|
|
permissionsRows[nameKey]->setAll(false);
|
|
*(permissionsRows[nameKey]) |= *perms;
|
|
}
|
|
}
|
|
|
|
void DomainServerSettingsManager::packPermissionsForMap(QString mapName,
|
|
NodePermissionsMap& permissionsRows,
|
|
QString keyPath) {
|
|
// grab a write lock on the settings mutex since we're about to change the config map
|
|
QWriteLocker locker(&_settingsLock);
|
|
|
|
// find (or create) the "security" section of the settings map
|
|
QVariant* security = _configMap.valueForKeyPath("security", true);
|
|
if (!security->canConvert(QMetaType::QVariantMap)) {
|
|
(*security) = QVariantMap();
|
|
}
|
|
|
|
// find (or create) whichever subsection of "security" we are packing
|
|
QVariant* permissions = _configMap.valueForKeyPath(keyPath, true);
|
|
if (!permissions->canConvert(QMetaType::QVariantList)) {
|
|
(*permissions) = QVariantList();
|
|
}
|
|
|
|
// convert details for each member of the subsection
|
|
QVariantList* permissionsList = reinterpret_cast<QVariantList*>(permissions);
|
|
(*permissionsList).clear();
|
|
QList<NodePermissionsKey> permissionsKeys = permissionsRows.keys();
|
|
|
|
// when a group is added from the domain-server settings page, the config map has a group-name with
|
|
// no ID or rank. We need to leave that there until we get a valid response back from the api.
|
|
// once we have the ranks and IDs, we need to delete the original entry so that it doesn't show
|
|
// up in the settings-page with undefined's after it.
|
|
QHash<QString, bool> groupNamesWithRanks;
|
|
// note which groups have rank/ID information
|
|
foreach (NodePermissionsKey userKey, permissionsKeys) {
|
|
NodePermissionsPointer perms = permissionsRows[userKey];
|
|
if (perms->getRankID() != QUuid()) {
|
|
groupNamesWithRanks[userKey.first] = true;
|
|
}
|
|
}
|
|
foreach (NodePermissionsKey userKey, permissionsKeys) {
|
|
NodePermissionsPointer perms = permissionsRows[userKey];
|
|
if (perms->isGroup()) {
|
|
QString groupName = userKey.first;
|
|
if (perms->getRankID() == QUuid() && groupNamesWithRanks.contains(groupName)) {
|
|
// copy the values from this user-added entry to the other (non-owner) ranks and remove it.
|
|
permissionsRows.remove(userKey);
|
|
initializeGroupPermissions(permissionsRows, groupName, perms);
|
|
}
|
|
}
|
|
}
|
|
|
|
// convert each group-name / rank-id pair to a variant-map
|
|
foreach (NodePermissionsKey userKey, permissionsKeys) {
|
|
if (!permissionsRows.contains(userKey)) {
|
|
continue;
|
|
}
|
|
NodePermissionsPointer perms = permissionsRows[userKey];
|
|
if (perms->isGroup()) {
|
|
QHash<QUuid, GroupRank>& groupRanks = _groupRanks[perms->getGroupID()];
|
|
*permissionsList += perms->toVariant(groupRanks);
|
|
} else {
|
|
*permissionsList += perms->toVariant();
|
|
}
|
|
}
|
|
}
|
|
|
|
void DomainServerSettingsManager::packPermissions() {
|
|
// transfer details from _agentPermissions to _configMap
|
|
|
|
// save settings for anonymous / logged-in / localhost
|
|
packPermissionsForMap("standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH);
|
|
|
|
// save settings for specific users
|
|
packPermissionsForMap("permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH);
|
|
|
|
// save settings for IP addresses
|
|
packPermissionsForMap("permissions", _ipPermissions, IP_PERMISSIONS_KEYPATH);
|
|
|
|
// save settings for MAC addresses
|
|
packPermissionsForMap("permissions", _macPermissions, MAC_PERMISSIONS_KEYPATH);
|
|
|
|
// save settings for Machine Fingerprint
|
|
packPermissionsForMap("permissions", _machineFingerprintPermissions, MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH);
|
|
|
|
// save settings for groups
|
|
packPermissionsForMap("permissions", _groupPermissions, GROUP_PERMISSIONS_KEYPATH);
|
|
|
|
// save settings for blacklist groups
|
|
packPermissionsForMap("permissions", _groupForbiddens, GROUP_FORBIDDENS_KEYPATH);
|
|
|
|
persistToFile();
|
|
}
|
|
|
|
bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& keyPath,
|
|
NodePermissionsMap* mapPointer,
|
|
std::function<void(NodePermissionsPointer)> customUnpacker) {
|
|
|
|
mapPointer->clear();
|
|
|
|
QVariant permissions = valueOrDefaultValueForKeyPath(keyPath);
|
|
|
|
if (!permissions.isValid()) {
|
|
// we don't have a permissions object to unpack for this keypath, bail
|
|
return false;
|
|
}
|
|
|
|
if (!permissions.canConvert(QMetaType::QVariantList)) {
|
|
qDebug() << "Failed to extract permissions for key path" << keyPath << "from settings.";
|
|
}
|
|
|
|
bool needPack = false;
|
|
|
|
QList<QVariant> permissionsList = permissions.toList();
|
|
foreach (QVariant permsHash, permissionsList) {
|
|
NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) };
|
|
QString id = perms->getID();
|
|
|
|
NodePermissionsKey idKey = perms->getKey();
|
|
|
|
if (mapPointer->contains(idKey)) {
|
|
qDebug() << "Duplicate name in permissions table for" << keyPath << " - " << id;
|
|
*((*mapPointer)[idKey]) |= *perms;
|
|
needPack = true;
|
|
} else {
|
|
(*mapPointer)[idKey] = perms;
|
|
}
|
|
|
|
if (customUnpacker) {
|
|
customUnpacker(perms);
|
|
}
|
|
}
|
|
|
|
return needPack;
|
|
|
|
}
|
|
|
|
void DomainServerSettingsManager::unpackPermissions() {
|
|
// transfer details from _configMap to _agentPermissions
|
|
|
|
// NOTE: Defaults for standard permissions (anonymous, friends, localhost, logged-in) used
|
|
// to be set here and then immediately persisted to the config JSON file.
|
|
// They have since been moved to describe-settings.json as the default value for AGENT_STANDARD_PERMISSIONS_KEYPATH.
|
|
// In order to change the default standard permissions you must change the default value in describe-settings.json.
|
|
|
|
bool needPack = false;
|
|
|
|
needPack |= unpackPermissionsForKeypath(AGENT_STANDARD_PERMISSIONS_KEYPATH, &_standardAgentPermissions);
|
|
|
|
needPack |= unpackPermissionsForKeypath(AGENT_PERMISSIONS_KEYPATH, &_agentPermissions);
|
|
|
|
needPack |= unpackPermissionsForKeypath(IP_PERMISSIONS_KEYPATH, &_ipPermissions,
|
|
[&](NodePermissionsPointer perms){
|
|
// make sure that this permission row is for a valid IP address
|
|
if (QHostAddress(perms->getKey().first).isNull()) {
|
|
_ipPermissions.remove(perms->getKey());
|
|
|
|
// we removed a row from the IP permissions, we'll need a re-pack
|
|
needPack = true;
|
|
}
|
|
});
|
|
|
|
needPack |= unpackPermissionsForKeypath(MAC_PERMISSIONS_KEYPATH, &_macPermissions,
|
|
[&](NodePermissionsPointer perms){
|
|
// make sure that this permission row is for a non-empty hardware
|
|
if (perms->getKey().first.isEmpty()) {
|
|
_macPermissions.remove(perms->getKey());
|
|
|
|
// we removed a row from the MAC permissions, we'll need a re-pack
|
|
needPack = true;
|
|
}
|
|
});
|
|
|
|
needPack |= unpackPermissionsForKeypath(MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH, &_machineFingerprintPermissions,
|
|
[&](NodePermissionsPointer perms){
|
|
// make sure that this permission row has valid machine fingerprint
|
|
if (QUuid(perms->getKey().first) == QUuid()) {
|
|
_machineFingerprintPermissions.remove(perms->getKey());
|
|
|
|
// we removed a row, so we'll need a re-pack
|
|
needPack = true;
|
|
}
|
|
|
|
});
|
|
|
|
|
|
needPack |= unpackPermissionsForKeypath(GROUP_PERMISSIONS_KEYPATH, &_groupPermissions,
|
|
[&](NodePermissionsPointer perms){
|
|
if (perms->isGroup()) {
|
|
// the group-id was cached. hook-up the uuid in the uuid->group hash
|
|
_groupPermissionsByUUID[GroupByUUIDKey(perms->getGroupID(), perms->getRankID())] = _groupPermissions[perms->getKey()];
|
|
needPack |= setGroupID(perms->getID(), perms->getGroupID());
|
|
}
|
|
});
|
|
|
|
needPack |= unpackPermissionsForKeypath(GROUP_FORBIDDENS_KEYPATH, &_groupForbiddens,
|
|
[&](NodePermissionsPointer perms) {
|
|
if (perms->isGroup()) {
|
|
// the group-id was cached. hook-up the uuid in the uuid->group hash
|
|
_groupForbiddensByUUID[GroupByUUIDKey(perms->getGroupID(), perms->getRankID())] = _groupForbiddens[perms->getKey()];
|
|
needPack |= setGroupID(perms->getID(), perms->getGroupID());
|
|
}
|
|
});
|
|
|
|
needPack |= ensurePermissionsForGroupRanks();
|
|
|
|
if (needPack) {
|
|
packPermissions();
|
|
}
|
|
|
|
#ifdef WANT_DEBUG
|
|
qDebug() << "--------------- permissions ---------------------";
|
|
std::list<NodePermissionsMap*> permissionsSets {
|
|
&_standardAgentPermissions, &_agentPermissions,
|
|
&_groupPermissions, &_groupForbiddens,
|
|
&_ipPermissions, &_macPermissions,
|
|
&_machineFingerprintPermissions
|
|
};
|
|
|
|
foreach (auto permissionSet, permissionsSets) {
|
|
auto& permissionKeyMap = permissionSet->get();
|
|
auto it = permissionKeyMap.begin();
|
|
|
|
while (it != permissionKeyMap.end()) {
|
|
|
|
NodePermissionsPointer perms = it->second;
|
|
if (perms->isGroup()) {
|
|
qDebug() << it->first << perms->getGroupID() << perms;
|
|
} else {
|
|
qDebug() << it->first << perms;
|
|
}
|
|
|
|
++it;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
}
|
|
|
|
bool DomainServerSettingsManager::ensurePermissionsForGroupRanks() {
|
|
// make sure each rank in each group has its own set of permissions
|
|
bool changed = false;
|
|
QList<QUuid> permissionGroupIDs = getGroupIDs();
|
|
foreach (QUuid groupID, permissionGroupIDs) {
|
|
QString groupName = _groupNames[groupID];
|
|
QHash<QUuid, GroupRank>& ranksForGroup = _groupRanks[groupID];
|
|
foreach (QUuid rankID, ranksForGroup.keys()) {
|
|
NodePermissionsKey nameKey = NodePermissionsKey(groupName, rankID);
|
|
GroupByUUIDKey idKey = GroupByUUIDKey(groupID, rankID);
|
|
NodePermissionsPointer perms;
|
|
if (_groupPermissions.contains(nameKey)) {
|
|
perms = _groupPermissions[nameKey];
|
|
} else {
|
|
perms = NodePermissionsPointer(new NodePermissions(nameKey));
|
|
_groupPermissions[nameKey] = perms;
|
|
changed = true;
|
|
}
|
|
if (perms->getGroupID() != groupID) {
|
|
perms->setGroupID(groupID);
|
|
changed = true;
|
|
}
|
|
if (perms->getRankID() != rankID) {
|
|
perms->setRankID(rankID);
|
|
changed = true;
|
|
}
|
|
_groupPermissionsByUUID[idKey] = perms;
|
|
}
|
|
}
|
|
|
|
QList<QUuid> forbiddenGroupIDs = getBlacklistGroupIDs();
|
|
foreach (QUuid groupID, forbiddenGroupIDs) {
|
|
QString groupName = _groupNames[groupID];
|
|
QHash<QUuid, GroupRank>& ranksForGroup = _groupRanks[groupID];
|
|
foreach (QUuid rankID, ranksForGroup.keys()) {
|
|
NodePermissionsKey nameKey = NodePermissionsKey(groupName, rankID);
|
|
GroupByUUIDKey idKey = GroupByUUIDKey(groupID, rankID);
|
|
NodePermissionsPointer perms;
|
|
if (_groupForbiddens.contains(nameKey)) {
|
|
perms = _groupForbiddens[nameKey];
|
|
} else {
|
|
perms = NodePermissionsPointer(new NodePermissions(nameKey));
|
|
_groupForbiddens[nameKey] = perms;
|
|
changed = true;
|
|
}
|
|
if (perms->getGroupID() != groupID) {
|
|
perms->setGroupID(groupID);
|
|
changed = true;
|
|
}
|
|
if (perms->getRankID() != rankID) {
|
|
perms->setRankID(rankID);
|
|
changed = true;
|
|
}
|
|
_groupForbiddensByUUID[idKey] = perms;
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
|
// before we do any processing on this packet make sure it comes from a node that is allowed to kick
|
|
if (sendingNode->getCanKick()) {
|
|
// pull the UUID being kicked from the packet
|
|
QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
|
|
|
if (!nodeUUID.isNull() && nodeUUID != sendingNode->getUUID()) {
|
|
// make sure we actually have a node with this UUID
|
|
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
|
|
|
auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID);
|
|
|
|
if (matchingNode) {
|
|
// we have a matching node, time to decide how to store updated permissions for this node
|
|
|
|
NodePermissionsPointer destinationPermissions;
|
|
|
|
auto verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
|
|
|
|
bool newPermissions = false;
|
|
|
|
if (!verifiedUsername.isEmpty()) {
|
|
// if we have a verified user name for this user, we first apply the kick to the username
|
|
|
|
// check if there were already permissions
|
|
bool hadPermissions = havePermissionsForName(verifiedUsername);
|
|
|
|
// grab or create permissions for the given username
|
|
auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
|
|
|
|
newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain);
|
|
|
|
// ensure that the connect permission is clear
|
|
userPermissions->clear(NodePermissions::Permission::canConnectToDomain);
|
|
}
|
|
|
|
// if we didn't have a username, or this domain-server uses the "multi-kick" setting to
|
|
// kick logged in users via username AND machine fingerprint (or IP as fallback)
|
|
// then we remove connect permissions for the machine fingerprint (or IP as fallback)
|
|
const QString MULTI_KICK_SETTINGS_KEYPATH = "security.multi_kick_logged_in";
|
|
|
|
if (verifiedUsername.isEmpty() || valueOrDefaultValueForKeyPath(MULTI_KICK_SETTINGS_KEYPATH).toBool()) {
|
|
// remove connect permissions for the machine fingerprint
|
|
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
|
if (nodeData) {
|
|
// get this machine's fingerprint
|
|
auto domainServerFingerprint = FingerprintUtils::getMachineFingerprint();
|
|
|
|
if (nodeData->getMachineFingerprint() == domainServerFingerprint) {
|
|
qWarning() << "attempt to kick node running on same machine as domain server (by fingerprint), ignoring KickRequest";
|
|
return;
|
|
}
|
|
NodePermissionsKey machineFingerprintKey(nodeData->getMachineFingerprint().toString(), 0);
|
|
|
|
// check if there were already permissions for the fingerprint
|
|
bool hadFingerprintPermissions = hasPermissionsForMachineFingerprint(nodeData->getMachineFingerprint());
|
|
|
|
// grab or create permissions for the given fingerprint
|
|
auto fingerprintPermissions = _machineFingerprintPermissions[machineFingerprintKey];
|
|
|
|
// write them
|
|
if (!hadFingerprintPermissions || fingerprintPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
|
|
newPermissions = true;
|
|
fingerprintPermissions->clear(NodePermissions::Permission::canConnectToDomain);
|
|
}
|
|
} else {
|
|
// if no node data, all we can do is IP address
|
|
auto& kickAddress = matchingNode->getActiveSocket()
|
|
? matchingNode->getActiveSocket()->getAddress()
|
|
: matchingNode->getPublicSocket().getAddress();
|
|
|
|
// probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server
|
|
// it is always 127.0.0.1, but looking at the public and local addresses just to be sure
|
|
// TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we
|
|
// do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it.
|
|
if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() ||
|
|
kickAddress == limitedNodeList->getLocalSockAddr().getAddress() ||
|
|
kickAddress.isLoopback() ) {
|
|
qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest";
|
|
return;
|
|
}
|
|
|
|
|
|
NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid());
|
|
|
|
// check if there were already permissions for the IP
|
|
bool hadIPPermissions = hasPermissionsForIP(kickAddress);
|
|
|
|
// grab or create permissions for the given IP address
|
|
auto ipPermissions = _ipPermissions[ipAddressKey];
|
|
|
|
if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
|
|
newPermissions = true;
|
|
|
|
ipPermissions->clear(NodePermissions::Permission::canConnectToDomain);
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we are here, then we kicked them, so send the KillAvatar message
|
|
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true);
|
|
packet->write(nodeUUID.toRfc4122());
|
|
packet->writePrimitive(KillAvatarReason::NoReason);
|
|
|
|
// send to avatar mixer, it sends the kill to everyone else
|
|
limitedNodeList->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer);
|
|
|
|
if (newPermissions) {
|
|
qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID())
|
|
<< "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID());
|
|
|
|
// we've changed permissions, time to store them to disk and emit our signal to say they have changed
|
|
packPermissions();
|
|
}
|
|
|
|
// we emit this no matter what -- though if this isn't a new permission probably 2 people are racing to kick and this
|
|
// person lost the race. No matter, just be sure this is called as otherwise it takes like 10s for the person being banned
|
|
// to go away
|
|
emit updateNodePermissions();
|
|
|
|
} else {
|
|
qWarning() << "Node kick request received for unknown node. Refusing to process.";
|
|
}
|
|
} else {
|
|
// this isn't a UUID we can use
|
|
qWarning() << "Node kick request received for invalid node ID or from node being kicked. Refusing to process.";
|
|
}
|
|
|
|
} else {
|
|
qWarning() << "Refusing to process a kick packet from node" << uuidStringWithoutCurlyBraces(sendingNode->getUUID())
|
|
<< "that does not have kick permissions.";
|
|
}
|
|
}
|
|
|
|
// This function processes the "Get Username from ID" request.
|
|
void DomainServerSettingsManager::processUsernameFromIDRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
|
// From the packet, pull the UUID we're identifying
|
|
QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
|
|
|
if (!nodeUUID.isNull()) {
|
|
// First, make sure we actually have a node with this UUID
|
|
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
|
|
auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID);
|
|
|
|
// If we do have a matching node...
|
|
if (matchingNode) {
|
|
// Setup the packet
|
|
auto usernameFromIDReplyPacket = NLPacket::create(PacketType::UsernameFromIDReply);
|
|
|
|
QString verifiedUsername;
|
|
QUuid machineFingerprint;
|
|
|
|
// Write the UUID to the packet
|
|
usernameFromIDReplyPacket->write(nodeUUID.toRfc4122());
|
|
|
|
// Check if the sending node has permission to kick (is an admin)
|
|
// OR if the message is from a node whose UUID matches the one in the packet
|
|
if (sendingNode->getCanKick() || nodeUUID == sendingNode->getUUID()) {
|
|
// It's time to figure out the username
|
|
verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
|
|
usernameFromIDReplyPacket->writeString(verifiedUsername);
|
|
|
|
// now put in the machine fingerprint
|
|
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
|
|
machineFingerprint = nodeData ? nodeData->getMachineFingerprint() : QUuid();
|
|
usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122());
|
|
} else {
|
|
usernameFromIDReplyPacket->writeString(verifiedUsername);
|
|
usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122());
|
|
}
|
|
// Write whether or not the user is an admin
|
|
bool isAdmin = matchingNode->getCanKick();
|
|
usernameFromIDReplyPacket->writePrimitive(isAdmin);
|
|
|
|
qDebug() << "Sending username" << verifiedUsername << "and machine fingerprint" << machineFingerprint << "associated with node" << nodeUUID << ". Node admin status: " << isAdmin;
|
|
// Ship it!
|
|
limitedNodeList->sendPacket(std::move(usernameFromIDReplyPacket), *sendingNode);
|
|
} else {
|
|
qWarning() << "Node username request received for unknown node. Refusing to process.";
|
|
}
|
|
} else {
|
|
qWarning() << "Node username request received for invalid node ID. Refusing to process.";
|
|
}
|
|
}
|
|
|
|
QStringList DomainServerSettingsManager::getAllNames() const {
|
|
QStringList result;
|
|
foreach (auto key, _agentPermissions.keys()) {
|
|
result << key.first.toLower();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const NodePermissionsKey& name) const {
|
|
if (_standardAgentPermissions.contains(name)) {
|
|
return *(_standardAgentPermissions[name].get());
|
|
}
|
|
NodePermissions nullPermissions;
|
|
nullPermissions.setAll(false);
|
|
return nullPermissions;
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const {
|
|
NodePermissionsKey nameKey = NodePermissionsKey(name, 0);
|
|
if (_agentPermissions.contains(nameKey)) {
|
|
return *(_agentPermissions[nameKey].get());
|
|
}
|
|
NodePermissions nullPermissions;
|
|
nullPermissions.setAll(false);
|
|
return nullPermissions;
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddress& address) const {
|
|
NodePermissionsKey ipKey = NodePermissionsKey(address.toString(), 0);
|
|
if (_ipPermissions.contains(ipKey)) {
|
|
return *(_ipPermissions[ipKey].get());
|
|
}
|
|
NodePermissions nullPermissions;
|
|
nullPermissions.setAll(false);
|
|
return nullPermissions;
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const {
|
|
NodePermissionsKey macKey = NodePermissionsKey(macAddress, 0);
|
|
if (_macPermissions.contains(macKey)) {
|
|
return *(_macPermissions[macKey].get());
|
|
}
|
|
NodePermissions nullPermissions;
|
|
nullPermissions.setAll(false);
|
|
return nullPermissions;
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getPermissionsForMachineFingerprint(const QUuid& machineFingerprint) const {
|
|
NodePermissionsKey fingerprintKey = NodePermissionsKey(machineFingerprint.toString(), 0);
|
|
if (_machineFingerprintPermissions.contains(fingerprintKey)) {
|
|
return *(_machineFingerprintPermissions[fingerprintKey].get());
|
|
}
|
|
NodePermissions nullPermissions;
|
|
nullPermissions.setAll(false);
|
|
return nullPermissions;
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QString& groupName, QUuid rankID) const {
|
|
NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID);
|
|
if (_groupPermissions.contains(groupRankKey)) {
|
|
return *(_groupPermissions[groupRankKey].get());
|
|
}
|
|
NodePermissions nullPermissions;
|
|
nullPermissions.setAll(false);
|
|
return nullPermissions;
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QUuid& groupID, QUuid rankID) const {
|
|
GroupByUUIDKey byUUIDKey = GroupByUUIDKey(groupID, rankID);
|
|
if (!_groupPermissionsByUUID.contains(byUUIDKey)) {
|
|
NodePermissions nullPermissions;
|
|
nullPermissions.setAll(false);
|
|
return nullPermissions;
|
|
}
|
|
NodePermissionsKey groupKey = _groupPermissionsByUUID[byUUIDKey]->getKey();
|
|
return getPermissionsForGroup(groupKey.first, groupKey.second);
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getForbiddensForGroup(const QString& groupName, QUuid rankID) const {
|
|
NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID);
|
|
if (_groupForbiddens.contains(groupRankKey)) {
|
|
return *(_groupForbiddens[groupRankKey].get());
|
|
}
|
|
NodePermissions allForbiddens;
|
|
allForbiddens.setAll(true);
|
|
return allForbiddens;
|
|
}
|
|
|
|
NodePermissions DomainServerSettingsManager::getForbiddensForGroup(const QUuid& groupID, QUuid rankID) const {
|
|
GroupByUUIDKey byUUIDKey = GroupByUUIDKey(groupID, rankID);
|
|
if (!_groupForbiddensByUUID.contains(byUUIDKey)) {
|
|
NodePermissions allForbiddens;
|
|
allForbiddens.setAll(true);
|
|
return allForbiddens;
|
|
}
|
|
|
|
NodePermissionsKey groupKey = _groupForbiddensByUUID[byUUIDKey]->getKey();
|
|
return getForbiddensForGroup(groupKey.first, groupKey.second);
|
|
}
|
|
|
|
QVariant DomainServerSettingsManager::valueForKeyPath(const QString& keyPath) {
|
|
QReadLocker locker(&_settingsLock);
|
|
auto foundValue = _configMap.valueForKeyPath(keyPath);
|
|
return foundValue ? *foundValue : QVariant();
|
|
}
|
|
|
|
QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) {
|
|
QReadLocker locker(&_settingsLock);
|
|
const QVariant* foundValue = _configMap.valueForKeyPath(keyPath);
|
|
|
|
if (foundValue) {
|
|
return *foundValue;
|
|
} else {
|
|
// we don't need the settings lock anymore since we're done reading from the config map
|
|
locker.unlock();
|
|
|
|
int dotIndex = keyPath.indexOf('.');
|
|
|
|
QString groupKey = keyPath.mid(0, dotIndex);
|
|
QString settingKey = keyPath.mid(dotIndex + 1);
|
|
|
|
foreach(const QVariant& group, _descriptionArray.toVariantList()) {
|
|
QVariantMap groupMap = group.toMap();
|
|
|
|
if (groupMap[DESCRIPTION_NAME_KEY].toString() == groupKey) {
|
|
foreach(const QVariant& setting, groupMap[DESCRIPTION_SETTINGS_KEY].toList()) {
|
|
QVariantMap settingMap = setting.toMap();
|
|
if (settingMap[DESCRIPTION_NAME_KEY].toString() == settingKey) {
|
|
return settingMap[SETTING_DEFAULT_KEY];
|
|
}
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
}
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) {
|
|
if (connection->requestOperation() == QNetworkAccessManager::PostOperation) {
|
|
static const QString SETTINGS_RESTORE_PATH = "/settings/restore";
|
|
|
|
if (url.path() == SETTINGS_PATH_JSON || url.path() == CONTENT_SETTINGS_PATH_JSON) {
|
|
// this is a POST operation to change one or more settings
|
|
QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent());
|
|
QJsonObject postedObject = postedDocument.object();
|
|
|
|
SettingsType endpointType = url.path() == SETTINGS_PATH_JSON ? DomainSettings : ContentSettings;
|
|
|
|
// we recurse one level deep below each group for the appropriate setting
|
|
bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject, endpointType);
|
|
|
|
// return success to the caller
|
|
QString jsonSuccess = "{\"status\": \"success\"}";
|
|
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
|
|
|
|
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
|
|
if (restartRequired) {
|
|
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
|
|
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
|
|
} else {
|
|
unpackPermissions();
|
|
apiRefreshGroupInformation();
|
|
emit updateNodePermissions();
|
|
emit settingsUpdated();
|
|
}
|
|
|
|
return true;
|
|
} else if (url.path() == SETTINGS_RESTORE_PATH) {
|
|
// this is an JSON settings file restore, ask the HTTPConnection to parse the data
|
|
QList<FormData> formData = connection->parseFormData();
|
|
|
|
bool wasRestoreSuccessful = false;
|
|
|
|
if (formData.size() > 0 && formData[0].second.size() > 0) {
|
|
// take the posted file and convert it to a QJsonObject
|
|
auto postedDocument = QJsonDocument::fromJson(formData[0].second);
|
|
if (postedDocument.isObject()) {
|
|
wasRestoreSuccessful = restoreSettingsFromObject(postedDocument.object(), DomainSettings);
|
|
}
|
|
}
|
|
|
|
if (wasRestoreSuccessful) {
|
|
// respond with a 200 for success
|
|
QString jsonSuccess = "{\"status\": \"success\"}";
|
|
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
|
|
|
|
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
|
|
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
|
|
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
|
|
} else {
|
|
// respond with a 400 for failure
|
|
connection->respond(HTTPConnection::StatusCode400);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation) {
|
|
static const QString SETTINGS_MENU_GROUPS_PATH = "/settings-menu-groups.json";
|
|
static const QString SETTINGS_BACKUP_PATH = "/settings/backup.json";
|
|
|
|
if (url.path() == SETTINGS_PATH_JSON || url.path() == CONTENT_SETTINGS_PATH_JSON) {
|
|
|
|
// setup a JSON Object with descriptions and non-omitted settings
|
|
const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions";
|
|
const QString SETTINGS_RESPONSE_VALUE_KEY = "values";
|
|
|
|
QJsonObject rootObject;
|
|
|
|
bool forDomainSettings = (url.path() == SETTINGS_PATH_JSON);
|
|
bool forContentSettings = (url.path() == CONTENT_SETTINGS_PATH_JSON);;
|
|
|
|
rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = forDomainSettings
|
|
? _domainSettingsDescription : _contentSettingsDescription;
|
|
|
|
// grab a domain settings object for all types, filtered for the right class of settings
|
|
// and exclude default values
|
|
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = settingsResponseObjectForType("", true,
|
|
forDomainSettings, forContentSettings,
|
|
true);
|
|
|
|
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
|
|
|
|
return true;
|
|
} else if (url.path() == SETTINGS_MENU_GROUPS_PATH) {
|
|
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(_settingsMenuGroups).toJson(), "application/json");
|
|
|
|
return true;
|
|
} else if (url.path() == SETTINGS_BACKUP_PATH) {
|
|
// grab the settings backup as an authenticated user
|
|
// for the domain settings type only, excluding hidden and default values
|
|
auto currentDomainSettingsJSON = settingsResponseObjectForType("", true, true, false, false, true);
|
|
|
|
// setup headers that tell the client to download the file wth a special name
|
|
Headers downloadHeaders;
|
|
downloadHeaders.insert("Content-Transfer-Encoding", "binary");
|
|
|
|
// create a timestamped filename for the backup
|
|
const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" };
|
|
auto backupFilename = "domain-settings_" + QDateTime::currentDateTime().toString(DATETIME_FORMAT) + ".json";
|
|
|
|
downloadHeaders.insert("Content-Disposition",
|
|
QString("attachment; filename=\"%1\"").arg(backupFilename).toLocal8Bit());
|
|
|
|
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(currentDomainSettingsJSON).toJson(),
|
|
"application/force-download", downloadHeaders);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) {
|
|
|
|
// grab a write lock since we're about to change the settings map
|
|
QWriteLocker locker(&_settingsLock);
|
|
|
|
QJsonArray* filteredDescriptionArray = settingsType == DomainSettings
|
|
? &_domainSettingsDescription : &_contentSettingsDescription;
|
|
|
|
// grab a copy of the current config before restore, so that we can back out if something bad happens during
|
|
QVariantMap preRestoreConfig = _configMap.getConfig();
|
|
|
|
bool shouldCancelRestore = false;
|
|
|
|
// enumerate through the settings in the description
|
|
// if we have one in the restore then use it, otherwise clear it from current settings
|
|
foreach(const QJsonValue& descriptionGroupValue, *filteredDescriptionArray) {
|
|
QJsonObject descriptionGroupObject = descriptionGroupValue.toObject();
|
|
QString groupKey = descriptionGroupObject[DESCRIPTION_NAME_KEY].toString();
|
|
QJsonArray descriptionGroupSettings = descriptionGroupObject[DESCRIPTION_SETTINGS_KEY].toArray();
|
|
|
|
// grab the matching group from the restore so we can look at its settings
|
|
QJsonObject restoreGroup;
|
|
QVariantMap* configGroupMap = nullptr;
|
|
|
|
if (groupKey.isEmpty()) {
|
|
// this is for a setting at the root, use the full object as our restore group
|
|
restoreGroup = settingsToRestore;
|
|
|
|
// the variant map for this "group" is just the config map since there's no group
|
|
configGroupMap = &_configMap.getConfig();
|
|
} else {
|
|
if (settingsToRestore.contains(groupKey)) {
|
|
restoreGroup = settingsToRestore[groupKey].toObject();
|
|
}
|
|
|
|
// grab the variant for the group
|
|
auto groupMapVariant = _configMap.valueForKeyPath(groupKey);
|
|
|
|
// if it existed, double check that it is a map - any other value is unexpected and should cancel a restore
|
|
if (groupMapVariant) {
|
|
if (groupMapVariant->canConvert<QVariantMap>()) {
|
|
configGroupMap = static_cast<QVariantMap*>(groupMapVariant->data());
|
|
} else {
|
|
shouldCancelRestore = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach(const QJsonValue& descriptionSettingValue, descriptionGroupSettings) {
|
|
|
|
QJsonObject descriptionSettingObject = descriptionSettingValue.toObject();
|
|
|
|
// we'll override this setting with the default or what is in the restore as long as
|
|
// it isn't specifically excluded from backups
|
|
bool isBackedUpSetting = !descriptionSettingObject.contains(DESCRIPTION_BACKUP_FLAG_KEY)
|
|
|| descriptionSettingObject[DESCRIPTION_BACKUP_FLAG_KEY].toBool();
|
|
|
|
if (isBackedUpSetting) {
|
|
QString settingName = descriptionSettingObject[DESCRIPTION_NAME_KEY].toString();
|
|
|
|
// check if we have a matching setting for this in the restore
|
|
QJsonValue restoreValue;
|
|
if (restoreGroup.contains(settingName)) {
|
|
restoreValue = restoreGroup[settingName];
|
|
}
|
|
|
|
// we should create the value for this key path in our current config map
|
|
// if we had value in the restore file
|
|
bool shouldCreateIfMissing = !restoreValue.isNull();
|
|
|
|
// get a QVariant pointer to this setting in our config map
|
|
QString fullSettingKey = !groupKey.isEmpty()
|
|
? groupKey + "." + settingName : settingName;
|
|
|
|
QVariant* variantValue = _configMap.valueForKeyPath(fullSettingKey, shouldCreateIfMissing);
|
|
|
|
if (restoreValue.isNull()) {
|
|
if (variantValue && !variantValue->isNull() && configGroupMap) {
|
|
// we didn't have a value to restore, but there might be a value in the config map
|
|
// so we need to remove the value in the config map which will set it back to the default
|
|
qDebug() << "Removing" << fullSettingKey << "from settings since it is not in the restored JSON";
|
|
configGroupMap->remove(settingName);
|
|
}
|
|
} else {
|
|
// we have a value to restore, use update setting to set it
|
|
// but clear the existing value first so that no merging between the restored settings
|
|
// and existing settings occurs
|
|
variantValue->clear();
|
|
|
|
// we might need to re-grab config group map in case it didn't exist when we looked for it before
|
|
// but was created by the call to valueForKeyPath before
|
|
if (!configGroupMap) {
|
|
auto groupMapVariant = _configMap.valueForKeyPath(groupKey);
|
|
if (groupMapVariant && groupMapVariant->canConvert<QVariantMap>()) {
|
|
configGroupMap = static_cast<QVariantMap*>(groupMapVariant->data());
|
|
} else {
|
|
shouldCancelRestore = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
qDebug() << "Updating setting" << fullSettingKey << "from restored JSON";
|
|
|
|
updateSetting(settingName, restoreValue, *configGroupMap, descriptionSettingObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldCancelRestore) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (shouldCancelRestore) {
|
|
// if we cancelled the restore, go back to our state before and return false
|
|
qDebug() << "Restore cancelled, settings have not been changed";
|
|
_configMap.getConfig() = preRestoreConfig;
|
|
return false;
|
|
} else {
|
|
// restore completed, persist the new settings
|
|
qDebug() << "Restore completed, persisting restored settings to file";
|
|
|
|
// let go of the write lock since we're done making changes to the config map
|
|
locker.unlock();
|
|
|
|
persistToFile();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated,
|
|
bool includeDomainSettings,
|
|
bool includeContentSettings,
|
|
bool includeDefaults, bool isForBackup) {
|
|
QJsonObject responseObject;
|
|
|
|
if (!typeValue.isEmpty() || isAuthenticated) {
|
|
// convert the string type value to a QJsonValue
|
|
QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt());
|
|
|
|
const QString AFFECTED_TYPES_JSON_KEY = "assignment-types";
|
|
|
|
// only enumerate the requested settings type (domain setting or content setting)
|
|
QJsonArray* filteredDescriptionArray = &_descriptionArray;
|
|
|
|
if (includeDomainSettings && !includeContentSettings) {
|
|
filteredDescriptionArray = &_domainSettingsDescription;
|
|
} else if (includeContentSettings && !includeDomainSettings) {
|
|
filteredDescriptionArray = &_contentSettingsDescription;
|
|
}
|
|
|
|
// enumerate the groups in the potentially filtered object to find which settings to pass
|
|
foreach(const QJsonValue& groupValue, *filteredDescriptionArray) {
|
|
QJsonObject groupObject = groupValue.toObject();
|
|
QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString();
|
|
QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray();
|
|
|
|
QJsonObject groupResponseObject;
|
|
|
|
foreach(const QJsonValue& settingValue, groupSettingsArray) {
|
|
|
|
const QString VALUE_HIDDEN_FLAG_KEY = "value-hidden";
|
|
|
|
QJsonObject settingObject = settingValue.toObject();
|
|
|
|
// consider this setting as long as it isn't hidden
|
|
// and either this isn't for a backup or it's a value included in backups
|
|
bool includedInBackups = !settingObject.contains(DESCRIPTION_BACKUP_FLAG_KEY)
|
|
|| settingObject[DESCRIPTION_BACKUP_FLAG_KEY].toBool();
|
|
|
|
if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool() && (!isForBackup || includedInBackups)) {
|
|
QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray();
|
|
if (affectedTypesArray.isEmpty()) {
|
|
affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray();
|
|
}
|
|
|
|
if (affectedTypesArray.contains(queryType) ||
|
|
(queryType.isNull() && isAuthenticated)) {
|
|
QString settingName = settingObject[DESCRIPTION_NAME_KEY].toString();
|
|
|
|
// we need to check if the settings map has a value for this setting
|
|
QVariant variantValue;
|
|
|
|
if (!groupKey.isEmpty()) {
|
|
QVariant settingsMapGroupValue = valueForKeyPath(groupKey);
|
|
|
|
if (!settingsMapGroupValue.isNull()) {
|
|
variantValue = settingsMapGroupValue.toMap().value(settingName);
|
|
}
|
|
} else {
|
|
variantValue = valueForKeyPath(settingName);
|
|
}
|
|
|
|
// final check for inclusion
|
|
// either we include default values or we don't but this isn't a default value
|
|
if (includeDefaults || variantValue.isValid()) {
|
|
QJsonValue result;
|
|
|
|
if (!variantValue.isValid()) {
|
|
// no value for this setting, pass the default
|
|
if (settingObject.contains(SETTING_DEFAULT_KEY)) {
|
|
result = settingObject[SETTING_DEFAULT_KEY];
|
|
} else {
|
|
// users are allowed not to provide a default for string values
|
|
// if so we set to the empty string
|
|
result = QString("");
|
|
}
|
|
} else {
|
|
result = QJsonValue::fromVariant(variantValue);
|
|
}
|
|
|
|
if (!groupKey.isEmpty()) {
|
|
// this belongs in the group object
|
|
groupResponseObject[settingName] = result;
|
|
} else {
|
|
// this is a value that should be at the root
|
|
responseObject[settingName] = result;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!groupKey.isEmpty() && !groupResponseObject.isEmpty()) {
|
|
// set this group's object to the constructed object
|
|
responseObject[groupKey] = groupResponseObject;
|
|
}
|
|
}
|
|
}
|
|
|
|
return responseObject;
|
|
}
|
|
|
|
void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap,
|
|
const QJsonObject& settingDescription) {
|
|
if (newValue.isString()) {
|
|
if (newValue.toString().isEmpty()) {
|
|
// this is an empty value, clear it in settings variant so the default is sent
|
|
settingMap.remove(key);
|
|
} else {
|
|
// make sure the resulting json value has the right type
|
|
QString settingType = settingDescription[SETTING_DESCRIPTION_TYPE_KEY].toString();
|
|
const QString INPUT_DOUBLE_TYPE = "double";
|
|
const QString INPUT_INTEGER_TYPE = "int";
|
|
|
|
if (settingType == INPUT_DOUBLE_TYPE) {
|
|
settingMap[key] = newValue.toString().toDouble();
|
|
} else if (settingType == INPUT_INTEGER_TYPE) {
|
|
settingMap[key] = newValue.toString().toInt();
|
|
} else {
|
|
QString sanitizedValue = newValue.toString();
|
|
|
|
// we perform special handling for viewpoints here
|
|
// we do not want them to be prepended with a slash
|
|
if (key == SETTINGS_VIEWPOINT_KEY && !sanitizedValue.startsWith('/')) {
|
|
sanitizedValue.prepend('/');
|
|
}
|
|
|
|
settingMap[key] = sanitizedValue;
|
|
}
|
|
}
|
|
} else if (newValue.isDouble()) {
|
|
settingMap[key] = newValue.toDouble();
|
|
} else if (newValue.isBool()) {
|
|
settingMap[key] = newValue.toBool();
|
|
} else if (newValue.isObject()) {
|
|
if (!settingMap.contains(key)) {
|
|
// we don't have a map below this key yet, so set it up now
|
|
settingMap[key] = QVariantMap();
|
|
}
|
|
|
|
QVariant& possibleMap = settingMap[key];
|
|
|
|
if (!possibleMap.canConvert(QMetaType::QVariantMap)) {
|
|
// if this isn't a map then we need to make it one, otherwise we're about to crash
|
|
qDebug() << "Value at" << key << "was not the expected QVariantMap while updating DS settings"
|
|
<< "- removing existing value and making it a QVariantMap";
|
|
possibleMap = QVariantMap();
|
|
}
|
|
|
|
QVariantMap& thisMap = *reinterpret_cast<QVariantMap*>(possibleMap.data());
|
|
foreach(const QString childKey, newValue.toObject().keys()) {
|
|
|
|
QJsonObject childDescriptionObject = settingDescription;
|
|
|
|
// is this the key? if so we have the description already
|
|
if (key != settingDescription[DESCRIPTION_NAME_KEY].toString()) {
|
|
// otherwise find the description object for this childKey under columns
|
|
foreach(const QJsonValue& column, settingDescription[DESCRIPTION_COLUMNS_KEY].toArray()) {
|
|
if (column.isObject()) {
|
|
QJsonObject thisDescription = column.toObject();
|
|
if (thisDescription[DESCRIPTION_NAME_KEY] == childKey) {
|
|
childDescriptionObject = column.toObject();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QString sanitizedKey = childKey;
|
|
|
|
if (key == SETTINGS_PATHS_KEY && !sanitizedKey.startsWith('/')) {
|
|
// We perform special handling for paths here.
|
|
// If we got sent a path without a leading slash then we add it.
|
|
sanitizedKey.prepend("/");
|
|
}
|
|
|
|
updateSetting(sanitizedKey, newValue.toObject()[childKey], thisMap, childDescriptionObject);
|
|
}
|
|
|
|
if (settingMap[key].toMap().isEmpty()) {
|
|
// we've cleared all of the settings below this value, so remove this one too
|
|
settingMap.remove(key);
|
|
}
|
|
} else if (newValue.isArray()) {
|
|
// we just assume array is replacement
|
|
// TODO: we still need to recurse here with the description in case values in the array have special types
|
|
settingMap[key] = newValue.toArray().toVariantList();
|
|
}
|
|
|
|
sortPermissions();
|
|
}
|
|
|
|
QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject,
|
|
const QString& settingName) {
|
|
foreach(const QJsonValue& settingValue, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) {
|
|
QJsonObject settingObject = settingValue.toObject();
|
|
if (settingObject[DESCRIPTION_NAME_KEY].toString() == settingName) {
|
|
return settingObject;
|
|
}
|
|
}
|
|
|
|
return QJsonObject();
|
|
}
|
|
|
|
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject,
|
|
SettingsType settingsType) {
|
|
|
|
// take a write lock since we're about to overwrite settings in the config map
|
|
QWriteLocker locker(&_settingsLock);
|
|
|
|
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";
|
|
|
|
auto& settingsVariant = _configMap.getConfig();
|
|
bool needRestart = false;
|
|
|
|
auto& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription;
|
|
|
|
// Iterate on the setting groups
|
|
foreach(const QString& rootKey, postedObject.keys()) {
|
|
const QJsonValue& rootValue = postedObject[rootKey];
|
|
|
|
if (!settingsVariant.contains(rootKey)) {
|
|
// we don't have a map below this key yet, so set it up now
|
|
settingsVariant[rootKey] = QVariantMap();
|
|
}
|
|
|
|
QVariantMap* thisMap = &settingsVariant;
|
|
|
|
QJsonObject groupDescriptionObject;
|
|
|
|
// we need to check the description array to see if this is a root setting or a group setting
|
|
foreach(const QJsonValue& groupValue, filteredDescriptionArray) {
|
|
if (groupValue.toObject()[DESCRIPTION_NAME_KEY] == rootKey) {
|
|
// we matched a group - keep this since we'll use it below to update the settings
|
|
groupDescriptionObject = groupValue.toObject();
|
|
|
|
// change the map we will update to be the map for this group
|
|
thisMap = reinterpret_cast<QVariantMap*>(settingsVariant[rootKey].data());
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (groupDescriptionObject.isEmpty()) {
|
|
// this is a root value, so we can call updateSetting for it directly
|
|
// first we need to find our description value for it
|
|
|
|
QJsonObject matchingDescriptionObject;
|
|
|
|
foreach(const QJsonValue& groupValue, _descriptionArray) {
|
|
// find groups with root values (they don't have a group name)
|
|
QJsonObject groupObject = groupValue.toObject();
|
|
|
|
if (!groupObject.contains(DESCRIPTION_NAME_KEY)) {
|
|
|
|
// this is a group with root values - check if our setting is in here
|
|
matchingDescriptionObject = settingDescriptionFromGroup(groupObject, rootKey);
|
|
|
|
if (!matchingDescriptionObject.isEmpty()) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!matchingDescriptionObject.isEmpty()) {
|
|
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
|
|
|
|
if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY &&
|
|
rootKey != SETTINGS_PATHS_KEY && rootKey != WIZARD_KEY) {
|
|
needRestart = true;
|
|
}
|
|
} else {
|
|
qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting.";
|
|
}
|
|
} else {
|
|
// this is a group - iterate on the settings in the group
|
|
foreach(const QString& settingKey, rootValue.toObject().keys()) {
|
|
// make sure this particular setting exists and we have a description object for it
|
|
QJsonObject matchingDescriptionObject = settingDescriptionFromGroup(groupDescriptionObject, settingKey);
|
|
|
|
// if we matched the setting then update the value
|
|
if (!matchingDescriptionObject.isEmpty()) {
|
|
const QJsonValue& settingValue = rootValue.toObject()[settingKey];
|
|
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
|
|
|
|
if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY &&
|
|
rootKey != DESCRIPTION_ROOT_KEY && rootKey != WIZARD_KEY) ||
|
|
settingKey == AC_SUBNET_WHITELIST_KEY) {
|
|
needRestart = true;
|
|
}
|
|
} else {
|
|
qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey <<
|
|
"- cannot update setting.";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (settingsVariant[rootKey].toMap().empty()) {
|
|
// we've cleared all of the settings below this value, so remove this one too
|
|
settingsVariant.remove(rootKey);
|
|
}
|
|
}
|
|
|
|
// we're done making changes to the config map, let go of our read lock
|
|
locker.unlock();
|
|
|
|
// store whatever the current config map is to file
|
|
persistToFile();
|
|
|
|
return needRestart;
|
|
}
|
|
|
|
// Compare two members of a permissions list
|
|
bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) {
|
|
if (!v1.canConvert(QMetaType::QVariantMap) ||
|
|
!v2.canConvert(QMetaType::QVariantMap)) {
|
|
return v1.toString() < v2.toString();
|
|
}
|
|
QVariantMap m1 = v1.toMap();
|
|
QVariantMap m2 = v2.toMap();
|
|
|
|
if (!m1.contains("permissions_id") ||
|
|
!m2.contains("permissions_id")) {
|
|
return v1.toString() < v2.toString();
|
|
}
|
|
|
|
if (m1.contains("rank_order") && m2.contains("rank_order") &&
|
|
m1["permissions_id"].toString() == m2["permissions_id"].toString()) {
|
|
return m1["rank_order"].toInt() < m2["rank_order"].toInt();
|
|
}
|
|
|
|
return m1["permissions_id"].toString() < m2["permissions_id"].toString();
|
|
}
|
|
|
|
void DomainServerSettingsManager::sortPermissions() {
|
|
// take a write lock since we're about to change the config map data
|
|
QWriteLocker locker(&_settingsLock);
|
|
|
|
// sort the permission-names
|
|
QVariant* standardPermissions = _configMap.valueForKeyPath(AGENT_STANDARD_PERMISSIONS_KEYPATH);
|
|
if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) {
|
|
QList<QVariant>* standardPermissionsList = reinterpret_cast<QVariantList*>(standardPermissions);
|
|
std::sort((*standardPermissionsList).begin(), (*standardPermissionsList).end(), permissionVariantLessThan);
|
|
}
|
|
QVariant* permissions = _configMap.valueForKeyPath(AGENT_PERMISSIONS_KEYPATH);
|
|
if (permissions && permissions->canConvert(QMetaType::QVariantList)) {
|
|
QList<QVariant>* permissionsList = reinterpret_cast<QVariantList*>(permissions);
|
|
std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan);
|
|
}
|
|
QVariant* groupPermissions = _configMap.valueForKeyPath(GROUP_PERMISSIONS_KEYPATH);
|
|
if (groupPermissions && groupPermissions->canConvert(QMetaType::QVariantList)) {
|
|
QList<QVariant>* permissionsList = reinterpret_cast<QVariantList*>(groupPermissions);
|
|
std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan);
|
|
}
|
|
QVariant* forbiddenPermissions = _configMap.valueForKeyPath(GROUP_FORBIDDENS_KEYPATH);
|
|
if (forbiddenPermissions && forbiddenPermissions->canConvert(QMetaType::QVariantList)) {
|
|
QList<QVariant>* permissionsList = reinterpret_cast<QVariantList*>(forbiddenPermissions);
|
|
std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan);
|
|
}
|
|
}
|
|
|
|
void DomainServerSettingsManager::persistToFile() {
|
|
sortPermissions();
|
|
|
|
// make sure we have the dir the settings file is supposed to live in
|
|
QFileInfo settingsFileInfo(_configMap.getUserConfigFilename());
|
|
|
|
if (!settingsFileInfo.dir().exists()) {
|
|
settingsFileInfo.dir().mkpath(".");
|
|
}
|
|
|
|
QFile settingsFile(_configMap.getUserConfigFilename());
|
|
|
|
if (settingsFile.open(QIODevice::WriteOnly)) {
|
|
// take a read lock so we can grab the config and write it to file
|
|
QReadLocker locker(&_settingsLock);
|
|
settingsFile.write(QJsonDocument::fromVariant(_configMap.getConfig()).toJson());
|
|
} else {
|
|
qCritical("Could not write to JSON settings file. Unable to persist settings.");
|
|
|
|
// failed to write, reload whatever the current config state is
|
|
// with a write lock since we're about to overwrite the config map
|
|
QWriteLocker locker(&_settingsLock);
|
|
_configMap.loadConfig(_argumentList);
|
|
}
|
|
}
|
|
|
|
QStringList DomainServerSettingsManager::getAllKnownGroupNames() {
|
|
// extract all the group names from the group-permissions and group-forbiddens settings
|
|
QSet<QString> result;
|
|
|
|
for (const auto& entry : _groupPermissions.get()) {
|
|
result += entry.first.first;
|
|
}
|
|
|
|
for (const auto& entry : _groupForbiddens.get()) {
|
|
result += entry.first.first;
|
|
}
|
|
|
|
return result.toList();
|
|
}
|
|
|
|
bool DomainServerSettingsManager::setGroupID(const QString& groupName, const QUuid& groupID) {
|
|
bool changed = false;
|
|
_groupIDs[groupName.toLower()] = groupID;
|
|
_groupNames[groupID] = groupName;
|
|
|
|
|
|
for (const auto& entry : _groupPermissions.get()) {
|
|
auto& perms = entry.second;
|
|
if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) {
|
|
changed = true;
|
|
perms->setGroupID(groupID);
|
|
}
|
|
}
|
|
|
|
for (const auto& entry : _groupForbiddens.get()) {
|
|
auto& perms = entry.second;
|
|
if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) {
|
|
changed = true;
|
|
perms->setGroupID(groupID);
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
void DomainServerSettingsManager::apiRefreshGroupInformation() {
|
|
if (!DependencyManager::get<AccountManager>()->hasAuthEndpoint()) {
|
|
// can't yet.
|
|
return;
|
|
}
|
|
|
|
bool changed = false;
|
|
|
|
QStringList groupNames = getAllKnownGroupNames();
|
|
foreach (QString groupName, groupNames) {
|
|
QString lowerGroupName = groupName.toLower();
|
|
if (_groupIDs.contains(lowerGroupName)) {
|
|
// we already know about this one. recall setGroupID in case the group has been
|
|
// added to another section (the same group is found in both groups and blacklists).
|
|
changed = setGroupID(groupName, _groupIDs[lowerGroupName]);
|
|
continue;
|
|
}
|
|
apiGetGroupID(groupName);
|
|
}
|
|
|
|
foreach (QUuid groupID, _groupNames.keys()) {
|
|
apiGetGroupRanks(groupID);
|
|
}
|
|
|
|
changed |= ensurePermissionsForGroupRanks();
|
|
|
|
if (changed) {
|
|
packPermissions();
|
|
}
|
|
|
|
unpackPermissions();
|
|
}
|
|
|
|
void DomainServerSettingsManager::apiGetGroupID(const QString& groupName) {
|
|
JSONCallbackParameters callbackParams;
|
|
callbackParams.jsonCallbackReceiver = this;
|
|
callbackParams.jsonCallbackMethod = "apiGetGroupIDJSONCallback";
|
|
callbackParams.errorCallbackReceiver = this;
|
|
callbackParams.errorCallbackMethod = "apiGetGroupIDErrorCallback";
|
|
|
|
const QString GET_GROUP_ID_PATH = "api/v1/groups/names/%1";
|
|
DependencyManager::get<AccountManager>()->sendRequest(GET_GROUP_ID_PATH.arg(groupName),
|
|
AccountManagerAuth::Required,
|
|
QNetworkAccessManager::GetOperation, callbackParams);
|
|
}
|
|
|
|
void DomainServerSettingsManager::apiGetGroupIDJSONCallback(QNetworkReply& requestReply) {
|
|
// {
|
|
// "data":{
|
|
// "groups":[{
|
|
// "description":null,
|
|
// "id":"fd55479a-265d-4990-854e-3d04214ad1b0",
|
|
// "is_list":false,
|
|
// "membership":{
|
|
// "permissions":{
|
|
// "custom_1=":false,
|
|
// "custom_2=":false,
|
|
// "custom_3=":false,
|
|
// "custom_4=":false,
|
|
// "del_group=":true,
|
|
// "invite_member=":true,
|
|
// "kick_member=":true,
|
|
// "list_members=":true,
|
|
// "mv_group=":true,
|
|
// "query_members=":true,
|
|
// "rank_member=":true
|
|
// },
|
|
// "rank":{
|
|
// "name=":"owner",
|
|
// "order=":0
|
|
// }
|
|
// },
|
|
// "name":"Blerg Blah"
|
|
// }]
|
|
// },
|
|
// "status":"success"
|
|
// }
|
|
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
|
|
if (jsonObject["status"].toString() == "success") {
|
|
QJsonArray groups = jsonObject["data"].toObject()["groups"].toArray();
|
|
for (int i = 0; i < groups.size(); i++) {
|
|
QJsonObject group = groups.at(i).toObject();
|
|
QString groupName = group["name"].toString();
|
|
QUuid groupID = QUuid(group["id"].toString());
|
|
|
|
bool changed = setGroupID(groupName, groupID);
|
|
if (changed) {
|
|
packPermissions();
|
|
apiGetGroupRanks(groupID);
|
|
}
|
|
}
|
|
} else {
|
|
qDebug() << "getGroupID api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact);
|
|
}
|
|
}
|
|
|
|
void DomainServerSettingsManager::apiGetGroupIDErrorCallback(QNetworkReply& requestReply) {
|
|
qDebug() << "******************** getGroupID api call failed:" << requestReply.error();
|
|
}
|
|
|
|
void DomainServerSettingsManager::apiGetGroupRanks(const QUuid& groupID) {
|
|
JSONCallbackParameters callbackParams;
|
|
callbackParams.jsonCallbackReceiver = this;
|
|
callbackParams.jsonCallbackMethod = "apiGetGroupRanksJSONCallback";
|
|
callbackParams.errorCallbackReceiver = this;
|
|
callbackParams.errorCallbackMethod = "apiGetGroupRanksErrorCallback";
|
|
|
|
const QString GET_GROUP_RANKS_PATH = "api/v1/groups/%1/ranks";
|
|
DependencyManager::get<AccountManager>()->sendRequest(GET_GROUP_RANKS_PATH.arg(groupID.toString().mid(1,36)),
|
|
AccountManagerAuth::Required,
|
|
QNetworkAccessManager::GetOperation, callbackParams);
|
|
}
|
|
|
|
void DomainServerSettingsManager::apiGetGroupRanksJSONCallback(QNetworkReply& requestReply) {
|
|
// {
|
|
// "data":{
|
|
// "groups":{
|
|
// "d3500f49-0655-4b1b-9846-ff8dd1b03351":{
|
|
// "members_count":1,
|
|
// "ranks":[
|
|
// {
|
|
// "id":"7979b774-e7f8-436c-9df1-912f1019f32f",
|
|
// "members_count":1,
|
|
// "name":"owner",
|
|
// "order":0,
|
|
// "permissions":{
|
|
// "custom_1":false,
|
|
// "custom_2":false,
|
|
// "custom_3":false,
|
|
// "custom_4":false,
|
|
// "edit_group":true,
|
|
// "edit_member":true,
|
|
// "edit_rank":true,
|
|
// "list_members":true,
|
|
// "list_permissions":true,
|
|
// "list_ranks":true,
|
|
// "query_member":true
|
|
// }
|
|
// }
|
|
// ]
|
|
// }
|
|
// }
|
|
// },"status":"success"
|
|
// }
|
|
|
|
bool changed = false;
|
|
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
|
|
|
|
if (jsonObject["status"].toString() == "success") {
|
|
QJsonObject groups = jsonObject["data"].toObject()["groups"].toObject();
|
|
foreach (auto groupID, groups.keys()) {
|
|
QJsonObject group = groups[groupID].toObject();
|
|
QJsonArray ranks = group["ranks"].toArray();
|
|
|
|
QHash<QUuid, GroupRank>& ranksForGroup = _groupRanks[groupID];
|
|
QHash<QUuid, bool> idsFromThisUpdate;
|
|
|
|
for (int rankIndex = 0; rankIndex < ranks.size(); rankIndex++) {
|
|
QJsonObject rank = ranks[rankIndex].toObject();
|
|
|
|
QUuid rankID = QUuid(rank["id"].toString());
|
|
int rankOrder = rank["order"].toInt();
|
|
QString rankName = rank["name"].toString();
|
|
int rankMembersCount = rank["members_count"].toInt();
|
|
|
|
GroupRank groupRank(rankID, rankOrder, rankName, rankMembersCount);
|
|
|
|
if (ranksForGroup[rankID] != groupRank) {
|
|
ranksForGroup[rankID] = groupRank;
|
|
changed = true;
|
|
}
|
|
|
|
idsFromThisUpdate[rankID] = true;
|
|
}
|
|
|
|
// clean up any that went away
|
|
foreach (QUuid rankID, ranksForGroup.keys()) {
|
|
if (!idsFromThisUpdate.contains(rankID)) {
|
|
ranksForGroup.remove(rankID);
|
|
}
|
|
}
|
|
}
|
|
|
|
changed |= ensurePermissionsForGroupRanks();
|
|
if (changed) {
|
|
packPermissions();
|
|
}
|
|
} else {
|
|
qDebug() << "getGroupRanks api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact);
|
|
}
|
|
}
|
|
|
|
void DomainServerSettingsManager::apiGetGroupRanksErrorCallback(QNetworkReply& requestReply) {
|
|
qDebug() << "******************** getGroupRanks api call failed:" << requestReply.error();
|
|
}
|
|
|
|
void DomainServerSettingsManager::recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID) {
|
|
if (rankID != QUuid()) {
|
|
_groupMembership[name.toLower()][groupID] = rankID;
|
|
} else {
|
|
_groupMembership[name.toLower()].remove(groupID);
|
|
}
|
|
}
|
|
|
|
QUuid DomainServerSettingsManager::isGroupMember(const QString& name, const QUuid& groupID) {
|
|
const QHash<QUuid, QUuid>& groupsForName = _groupMembership[name.toLower()];
|
|
if (groupsForName.contains(groupID)) {
|
|
return groupsForName[groupID];
|
|
}
|
|
return QUuid();
|
|
}
|
|
|
|
QList<QUuid> DomainServerSettingsManager::getGroupIDs() {
|
|
QSet<QUuid> result;
|
|
foreach (NodePermissionsKey groupKey, _groupPermissions.keys()) {
|
|
if (_groupPermissions[groupKey]->isGroup()) {
|
|
result += _groupPermissions[groupKey]->getGroupID();
|
|
}
|
|
}
|
|
return result.toList();
|
|
}
|
|
|
|
QList<QUuid> DomainServerSettingsManager::getBlacklistGroupIDs() {
|
|
QSet<QUuid> result;
|
|
foreach (NodePermissionsKey groupKey, _groupForbiddens.keys()) {
|
|
if (_groupForbiddens[groupKey]->isGroup()) {
|
|
result += _groupForbiddens[groupKey]->getGroupID();
|
|
}
|
|
}
|
|
return result.toList();
|
|
}
|
|
|
|
void DomainServerSettingsManager::debugDumpGroupsState() {
|
|
qDebug() << "--------- GROUPS ---------";
|
|
|
|
qDebug() << "_groupPermissions:";
|
|
foreach (NodePermissionsKey groupKey, _groupPermissions.keys()) {
|
|
NodePermissionsPointer perms = _groupPermissions[groupKey];
|
|
qDebug() << "| " << groupKey << perms;
|
|
}
|
|
|
|
qDebug() << "_groupForbiddens:";
|
|
foreach (NodePermissionsKey groupKey, _groupForbiddens.keys()) {
|
|
NodePermissionsPointer perms = _groupForbiddens[groupKey];
|
|
qDebug() << "| " << groupKey << perms;
|
|
}
|
|
|
|
qDebug() << "_groupIDs:";
|
|
foreach (QString groupName, _groupIDs.keys()) {
|
|
qDebug() << "| " << groupName << "==>" << _groupIDs[groupName.toLower()];
|
|
}
|
|
|
|
qDebug() << "_groupNames:";
|
|
foreach (QUuid groupID, _groupNames.keys()) {
|
|
qDebug() << "| " << groupID << "==>" << _groupNames[groupID];
|
|
}
|
|
|
|
qDebug() << "_groupRanks:";
|
|
foreach (QUuid groupID, _groupRanks.keys()) {
|
|
QHash<QUuid, GroupRank>& ranksForGroup = _groupRanks[groupID];
|
|
qDebug() << "| " << groupID;
|
|
foreach (QUuid rankID, ranksForGroup.keys()) {
|
|
QString rankName = ranksForGroup[rankID].name;
|
|
qDebug() << "| " << rankID << rankName;
|
|
}
|
|
}
|
|
|
|
qDebug() << "_groupMembership";
|
|
foreach (QString userName, _groupMembership.keys()) {
|
|
QHash<QUuid, QUuid>& groupsForUser = _groupMembership[userName.toLower()];
|
|
QString line = "";
|
|
foreach (QUuid groupID, groupsForUser.keys()) {
|
|
line += " g=" + groupID.toString() + ",r=" + groupsForUser[groupID].toString();
|
|
}
|
|
qDebug() << "| " << userName << line;
|
|
}
|
|
}
|