diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 281a0be9cc..8e57a9b683 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -70,25 +70,23 @@ "advanced": true }, { - "name": "require_oauth2", - "label": "Require OAuth2 Authentication", - "help": "For any users not explicitly authorized in these settings, notify the Interface to authenticate through this method.", - "default": false, - "type": "checkbox", + "name": "oauth2_url_path", + "label": "Authentication URL", + "help": "The URL that the Interface will use to login via OAuth2.", "advanced": true }, { - "name": "domain_access_token", - "label": "Domain API Access Token", - "help": "This is the access token that your domain-server will use to verify users and their roles. This token must grant access to that permission set on your REST API server.", + "name": "wordpress_url_base", + "label": "WordPress API URL Base", + "help": "The URL base that the domain server will use to make WordPress API calls. Typically \"https://oursite.com/wp-json/\". However, if using non-pretty permalinks or otherwise get a 404 error then try \"https://oursite.com/?rest_route=/\".", + "advanced": true + }, + { + "name": "plugin_client_id", + "label": "WordPress Plugin Client ID", + "help": "This is the client ID from the WordPress plugin configuration.", "advanced": true, "backup": false - }, - { - "name": "authentication_oauth2_url_base", - "label": "Authentication URL Base", - "help": "The URL base that the Interface and domain-server will use to make API requests.", - "advanced": true } ] }, diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 75cb11dab7..5b0c526d9f 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -90,12 +90,13 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointersecond); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QByteArray usernameSignature; - QByteArray domainUsernameSignature; + + QString domainUsername; + QStringList domainTokens; if (message->getBytesLeftToRead() > 0) { // read username from packet @@ -108,16 +109,21 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetBytesLeftToRead() > 0) { // Read domain username from packet. packetStream >> domainUsername; + domainUsername = domainUsername.toLower(); // Domain usernames are case-insensitive; internally lower-case. if (message->getBytesLeftToRead() > 0) { - // Read domain signature from packet. - packetStream >> domainUsernameSignature; + // Read domain tokens from packet. + + QString domainTokensString; + packetStream >> domainTokensString; + domainTokens = domainTokensString.split(":"); } } } } - node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainUsernameSignature); + node = processAgentConnectRequest(nodeConnection, username, usernameSignature, + domainUsername, domainTokens.value(0), domainTokens.value(1)); } if (node) { @@ -156,10 +162,8 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer_settingsManager.getAllKnownGroupNames().contains(group)) { - userPerms |= _server->_settingsManager.getPermissionsForGroup(group, QUuid()); -//#ifdef WANT_DEBUG - qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << group << "so:" << userPerms; -//#endif - + if (!verifiedDomainUserName.isEmpty()) { + auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; + foreach (QString userGroup, userGroups) { + // Domain groups may be specified as comma- and/or space-separated lists of group names. + // For example, "@silver @Gold, @platinum". + auto domainGroups = _server->_settingsManager.getDomainGroupNames() + .filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$", + QRegularExpression::CaseInsensitiveOption)); + foreach(QString domainGroup, domainGroups) { + userPerms |= _server->_settingsManager.getPermissionsForGroup(domainGroup, QUuid()); // No rank for domain groups. +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << domainGroup + << "so:" << userPerms; +#endif } } - userPerms.setVerifiedDomainUserName(verifiedDomainUserName); - userPerms.setVerifiedDomainUserGroups(verifiedDomainUserGroups); } if (verifiedUsername.isEmpty()) { @@ -289,6 +297,26 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin userPerms.setVerifiedUserName(verifiedUsername); } + // If this user is a known member of an domain group that is blacklisted, remove the implied permissions. + if (!verifiedDomainUserName.isEmpty()) { + auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; + foreach(QString userGroup, userGroups) { + // Domain groups may be specified as comma- and/or space-separated lists of group names. + // For example, "@silver @Gold, @platinum". + auto domainGroups = _server->_settingsManager.getDomainBlacklistGroupNames() + .filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$", + QRegularExpression::CaseInsensitiveOption)); + foreach(QString domainGroup, domainGroups) { + userPerms &= ~_server->_settingsManager.getForbiddensForGroup(domainGroup, QUuid()); +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: domain user is in blacklist group:" << domainGroup << "so:" << userPerms; +#endif + } + } + + userPerms.setVerifiedDomainUserName(verifiedDomainUserName); + } + #ifdef WANT_DEBUG qDebug() << "| user-permissions: final:" << userPerms; #endif @@ -309,7 +337,6 @@ void DomainGatekeeper::updateNodePermissions() { // authentication and verifiedUsername is only set once they user's key has been confirmed. QString verifiedUsername = node->getPermissions().getVerifiedUserName(); QString verifiedDomainUserName = node->getPermissions().getVerifiedDomainUserName(); - QStringList verifiedDomainUserGroups = node->getPermissions().getVerifiedDomainUserGroups(); NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0)); if (node->getPermissions().isAssignment) { @@ -345,8 +372,7 @@ void DomainGatekeeper::updateNodePermissions() { } userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUserName, - verifiedDomainUserGroups, connectingAddr.getAddress(), - hardwareAddress, machineFingerprint); + connectingAddr.getAddress(), hardwareAddress, machineFingerprint); } node->setPermissions(userPerms); @@ -424,6 +450,10 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo return newNode; } +const QString AUTHENTICATION_ENAABLED = "authentication.enable_oauth2"; +const QString AUTHENTICATION_OAUTH2_URL_PATH = "authentication.oauth2_url_path"; +const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; +const QString AUTHENTICATION_PLUGIN_CLIENT_ID = "authentication.plugin_client_id"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; @@ -431,7 +461,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QByteArray& domainUsernameSignature) { + const QString& domainAccessToken, + const QString& domainRefreshToken) { auto limitedNodeList = DependencyManager::get(); @@ -481,45 +512,58 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect QString verifiedDomainUsername; QStringList verifiedDomainUserGroups; if (domainHasLogin() && !domainUsername.isEmpty()) { - if (domainUsernameSignature.isEmpty()) { + + if (domainAccessToken.isEmpty()) { // User is attempting to prove their domain identity. - - // ####### TODO: OAuth2 corollary of metaverse code, above. - - return SharedNodePointer(); - } else if (verifyDomainUserSignature(domainUsername, domainUsernameSignature, nodeConnection.senderSockAddr)) { - // User's domain identity is confirmed. - - // ####### TODO: Get user's domain group memberships (WordPress roles) from domain. - // This may already be provided at the same time as the "verify" call to the domain API. - // If it isn't, need to initiate getting them then handle their receipt along the lines of the - // metaverse code, above. - verifiedDomainUserGroups = QString("test-group").toLower().split(" "); - - verifiedDomainUsername = domainUsername.toLower(); - - } else { - // User's identity didn't check out. - - // ####### TODO: OAuth2 corollary of metaverse code, above. - #ifdef WANT_DEBUG - qDebug() << "stalling login because signature verification failed:" << username; + qDebug() << "Stalling login because we have no domain OAuth2 tokens:" << domainUsername; #endif return SharedNodePointer(); + + } else if (needToVerifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken)) { + // User's domain identity needs to be confirmed. + requestDomainUser(domainUsername, domainAccessToken, domainRefreshToken); +#ifdef WANT_DEBUG + qDebug() << "Stalling login because we haven't authenticated user yet:" << domainUsername; +#endif + + } else if (verifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken, + nodeConnection.senderSockAddr)) { + // User's domain identity is confirmed. + verifiedDomainUsername = domainUsername; + + } else { + // User's domain identity didn't check out. +#ifdef WANT_DEBUG + qDebug() << "Stalling login because domain user verification failed:" << domainUsername; +#endif + return SharedNodePointer(); + } } - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUsername, verifiedDomainUserGroups, + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUsername, nodeConnection.senderSockAddr.getAddress(), nodeConnection.hardwareAddress, nodeConnection.machineFingerprint); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { if (domainHasLogin()) { - sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", - nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain); + QString domainAuthURL; + auto domainAuthURLVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH); + if (domainAuthURLVariant.canConvert()) { + domainAuthURL = domainAuthURLVariant.toString(); + } + QString domainAuthClientID; + auto domainAuthClientIDVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_PLUGIN_CLIENT_ID); + if (domainAuthClientIDVariant.canConvert()) { + domainAuthClientID = domainAuthClientIDVariant.toString(); + } + + sendConnectionDeniedPacket("You lack the required domain permissions to connect to this domain.", + nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, + domainAuthURL + "|" + domainAuthClientID); } else { - sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", + sendConnectionDeniedPacket("You lack the required metaverse permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedMetaverse); } #ifdef WANT_DEBUG @@ -717,17 +761,21 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, return false; } -bool DomainGatekeeper::verifyDomainUserSignature(const QString& domainUsername, - const QByteArray& domainUsernameSignature, - const HifiSockAddr& senderSockAddr) { - // ####### TODO: Verify via domain OAuth2. - bool success = true; - if (success) { +bool DomainGatekeeper::needToVerifyDomainUserIdentity(const QString& username, const QString& accessToken, + const QString& refreshToken) { + return !_verifiedDomainUserIdentities.contains(username) + || _verifiedDomainUserIdentities.value(username) != QPair(accessToken, refreshToken); +} + +bool DomainGatekeeper::verifyDomainUserIdentity(const QString& username, const QString& accessToken, + const QString& refreshToken, const HifiSockAddr& senderSockAddr) { + if (_verifiedDomainUserIdentities.contains(username) + && _verifiedDomainUserIdentities.value(username) == QPair(accessToken, refreshToken)) { return true; } - sendConnectionDeniedPacket("Error decrypting domain username signature.", senderSockAddr, + sendConnectionDeniedPacket("Error verifying domain user.", senderSockAddr, DomainHandler::ConnectionRefusedReason::LoginErrorDomain); return false; } @@ -1004,7 +1052,6 @@ void DomainGatekeeper::getGroupMemberships(const QString& username) { AccountManagerAuth::Required, QNetworkAccessManager::PostOperation, callbackParams, QJsonDocument(json).toJson()); - } QString extractUsernameFromGroupMembershipsReply(QNetworkReply* requestReply) { @@ -1059,6 +1106,7 @@ void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply _inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply)); } + void DomainGatekeeper::getDomainOwnerFriendsList() { JSONCallbackParameters callbackParams; callbackParams.callbackReceiver = this; @@ -1107,6 +1155,7 @@ void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply* req qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply->error(); } +// ####### TODO: Domain equivalent or addition [plugin groups] void DomainGatekeeper::refreshGroupsCache() { // if agents are connected to this domain, refresh our cached information about groups and memberships in such. getDomainOwnerFriendsList(); @@ -1131,17 +1180,6 @@ void DomainGatekeeper::refreshGroupsCache() { #endif } -bool DomainGatekeeper::domainHasLogin() { - // The domain may have its own users and groups. This is enabled in the server settings by ... - // ####### TODO: Use a particular string in the server name or set a particular tag in the server's settings? - // Or add a new server setting? - - // ####### TODO: Also configure URL for getting user's group memberships, in the server's settings? - - // ####### TODO - return true; -} - void DomainGatekeeper::initLocalIDManagement() { std::uniform_int_distribution sixteenBitRand; std::random_device randomDevice; @@ -1169,3 +1207,91 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) { _localIDs.insert(newLocalID); return newLocalID; } + + +bool DomainGatekeeper::domainHasLogin() { + // The domain may have its own users and groups in a WordPress site. + // ####### TODO: Add checks of any further domain server settings used. [plugin, groups] + return _server->_settingsManager.valueForKeyPath(AUTHENTICATION_ENAABLED).toBool() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH).toString().isEmpty() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString().isEmpty(); +} + +void DomainGatekeeper::requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken) { + + if (_inFlightDomainUserIdentityRequests.contains(username)) { + // Domain identify request for this username is already in progress. + return; + } + _inFlightDomainUserIdentityRequests.insert(username, QPair(accessToken, refreshToken)); + + if (_verifiedDomainUserIdentities.contains(username)) { + _verifiedDomainUserIdentities.remove(username); + } + + QString apiBase = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString(); + if (!apiBase.endsWith("/")) { + apiBase += "/"; + } + + // Get data pertaining to "me", the user who generated the access token. + const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me"; + const QString WORDPRESS_USER_QUERY = "_fields=username,roles"; + QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE + (apiBase.contains("?") ? "&" : "?") + WORDPRESS_USER_QUERY; + + QNetworkRequest request; + + request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setRawHeader(QByteArray("Authorization"), QString("Bearer " + accessToken).toUtf8()); + + QByteArray formData; // No data to send. + + request.setUrl(domainUserURL); + + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* requestReply = networkAccessManager.post(request, formData); + connect(requestReply, &QNetworkReply::finished, this, &DomainGatekeeper::requestDomainUserFinished); +} + +void DomainGatekeeper::requestDomainUserFinished() { + + QNetworkReply* requestReply = reinterpret_cast(sender()); + + QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + + auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (200 <= httpStatus && httpStatus < 300) { + + QString username = rootObject.value("username").toString().toLower(); + if (_inFlightDomainUserIdentityRequests.contains(username)) { + // Success! Verified user. + _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); + _inFlightDomainUserIdentityRequests.remove(username); + + // User user's WordPress roles as domain groups. + QStringList domainUserGroups; + auto userRoles = rootObject.value("roles").toArray(); + foreach (auto role, userRoles) { + // Distinguish domain groups from metaverse groups by a leading special character. + domainUserGroups.append(DOMAIN_GROUP_CHAR + role.toString().toLower()); + } + _domainGroupMemberships[username] = domainUserGroups; + + } else { + // Failure. + qDebug() << "Unexpected username in response for user details -" << username; + } + + } else { + // Failure. + qDebug() << "Error in response for user details -" << httpStatus << requestReply->error() + << "-" << rootObject["error"].toString() << rootObject["error_description"].toString(); + + _inFlightDomainUserIdentityRequests.clear(); + } +} diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 172c63028c..cb42baa7e3 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -30,6 +30,8 @@ #include "NodeConnectionData.h" #include "PendingAssignedNodeData.h" +const QString DOMAIN_GROUP_CHAR = "@"; + class DomainServer; class DomainGatekeeper : public QObject { @@ -72,6 +74,10 @@ public slots: private slots: void handlePeerPingTimeout(); + + // Login and groups for domain, separate from metaverse. + void requestDomainUserFinished(); + private: SharedNodePointer processAssignmentConnectRequest(const NodeConnectionData& nodeConnection, const PendingAssignedNodeData& pendingAssignment); @@ -79,14 +85,16 @@ private: const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QByteArray& domainUsernameSignature); + const QString& domainAccessToken, + const QString& domainRefreshToken); SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection); bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); - bool verifyDomainUserSignature(const QString& domainUsername, const QByteArray& domainUsernameSignature, - const HifiSockAddr& senderSockAddr); + bool needToVerifyDomainUserIdentity(const QString& username, const QString& accessToken, const QString& refreshToken); + bool verifyDomainUserIdentity(const QString& username, const QString& accessToken, const QString& refreshToken, + const HifiSockAddr& senderSockAddr); bool isWithinMaxCapacity(); @@ -128,24 +136,31 @@ private: QSet _inFlightGroupMembershipsRequests; // keep track of which we've already asked for NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, QString verifiedDomainUsername, - QStringList verifiedDomainUserGroups, const QHostAddress& senderAddress, - const QString& hardwareAddress, const QUuid& machineFingerprint); + const QHostAddress& senderAddress, const QString& hardwareAddress, + const QUuid& machineFingerprint); void getGroupMemberships(const QString& username); // void getIsGroupMember(const QString& username, const QUuid groupID); void getDomainOwnerFriendsList(); - bool domainHasLogin(); - // Local ID management. void initLocalIDManagement(); using UUIDToLocalID = std::unordered_map ; using LocalIDs = std::unordered_set; LocalIDs _localIDs; UUIDToLocalID _uuidToLocalID; - Node::LocalID _currentLocalID; Node::LocalID _idIncrement; + + // Login and groups for domain, separate from metaverse. + bool domainHasLogin(); + void requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken); + + typedef QHash> DomainUserIdentities; // > + DomainUserIdentities _inFlightDomainUserIdentityRequests; // Domain user identity requests currently in progress. + DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users. + + QHash _domainGroupMemberships; // }; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 73d78a5c70..c03f26f0a1 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1966,6 +1966,10 @@ void DomainServerSettingsManager::apiRefreshGroupInformation() { QStringList groupNames = getAllKnownGroupNames(); foreach (QString groupName, groupNames) { QString lowerGroupName = groupName.toLower(); + if (lowerGroupName.contains(DOMAIN_GROUP_CHAR)) { + // Ignore domain groups. + return; + } 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). @@ -2185,6 +2189,24 @@ QList DomainServerSettingsManager::getBlacklistGroupIDs() { return result.toList(); } +QStringList DomainServerSettingsManager::getDomainGroupNames() { + // Names as configured in domain server; not necessarily metaverse groups. + QSet result; + foreach(NodePermissionsKey groupKey, _groupPermissions.keys()) { + result += _groupPermissions[groupKey]->getID(); + } + return result.toList(); +} + +QStringList DomainServerSettingsManager::getDomainBlacklistGroupNames() { + // Names as configured in domain server; not necessarily mnetaverse groups. + QSet result; + foreach (NodePermissionsKey groupKey, _groupForbiddens.keys()) { + result += _groupForbiddens[groupKey]->getID(); + } + return result.toList(); +} + void DomainServerSettingsManager::debugDumpGroupsState() { qDebug() << "--------- GROUPS ---------"; diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index e28b9f6cd1..73aef07835 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -19,11 +19,11 @@ #include #include - -#include -#include "NodePermissions.h" - #include +#include + +#include "DomainGatekeeper.h" +#include "NodePermissions.h" const QString SETTINGS_PATHS_KEY = "paths"; @@ -105,6 +105,9 @@ public: QList getGroupIDs(); QList getBlacklistGroupIDs(); + QStringList getDomainGroupNames(); + QStringList getDomainBlacklistGroupNames(); + // these are used to locally cache the result of calling "api/v1/groups/.../is_member/..." on metaverse's api void clearGroupMemberships(const QString& name) { _groupMembership[name.toLower()].clear(); } void recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID); diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index ec3f0d263e..8d86963578 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -47,7 +47,7 @@ Item { readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() // If not logging into domain, then we must be logging into the metaverse... readonly property bool isLoggingInToDomain: loginDialog.getDomainLoginRequested() - readonly property string domainAuthProvider: loginDialog.getDomainLoginAuthProvider() + readonly property string domainLoginDomain: loginDialog.getDomainLoginDomain() QtObject { id: d @@ -77,7 +77,7 @@ Item { if (!isLoggingInToDomain) { loginDialog.login(emailField.text, passwordField.text); } else { - loginDialog.loginDomain(emailField.text, passwordField.text, domainAuthProvider); + loginDialog.loginDomain(emailField.text, passwordField.text); } if (linkAccountBody.loginDialogPoppedUp) { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 72e6af74ec..2c6702fbfc 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -9444,7 +9444,6 @@ void Application::forceDisplayName(const QString& displayName) { getMyAvatar()->setDisplayName(displayName); } void Application::forceLoginWithTokens(const QString& tokens) { - // ####### TODO DependencyManager::get()->setAccessTokens(tokens); Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, true).set(true); } diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index 33d9fddc1b..070015f05b 100644 --- a/interface/src/ConnectionMonitor.cpp +++ b/interface/src/ConnectionMonitor.cpp @@ -14,8 +14,10 @@ #include "Application.h" #include "ui/DialogsManager.h" +#include #include #include +#include #include #include @@ -34,6 +36,10 @@ void ConnectionMonitor::init() { connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ConnectionMonitor::stopTimer); connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &ConnectionMonitor::stopTimer); connect(this, &ConnectionMonitor::setRedirectErrorState, &domainHandler, &DomainHandler::setRedirectErrorState); + auto accountManager = DependencyManager::get(); + connect(accountManager.data(), &AccountManager::loginComplete, this, &ConnectionMonitor::startTimer); + auto domainAccountManager = DependencyManager::get(); + connect(domainAccountManager.data(), &DomainAccountManager::loginComplete, this, &ConnectionMonitor::startTimer); _timer.setSingleShot(true); if (!domainHandler.isConnected()) { diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index c61d4c4069..6e807c7b9f 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -29,7 +29,6 @@ #include "OctreeStatsDialog.h" #include "PreferencesDialog.h" #include "UpdateDialog.h" -#include "DomainHandler.h" #include "scripting/HMDScriptingInterface.h" @@ -111,8 +110,14 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { } } + +void DialogsManager::setDomainLogin(bool isDomainLogin, const QString& domain) { + _isDomainLogin = isDomainLogin; + _domainLoginDomain = domain; +} + void DialogsManager::toggleLoginDialog() { - _isDomainLogin = false; + setDomainLogin(false); LoginDialog::toggleAction(); } @@ -121,7 +126,7 @@ void DialogsManager::showLoginDialog() { // ####### TODO: May be called from script via DialogsManagerScriptingInterface. Need to handle the case that it's already // displayed and may be the domain login version. - _isDomainLogin = false; + setDomainLogin(false); LoginDialog::showWithSelection(); } @@ -130,20 +135,16 @@ void DialogsManager::hideLoginDialog() { } -void DialogsManager::showDomainLoginDialog() { - const QJsonObject& settingsObject = DependencyManager::get()->getDomainHandler().getSettingsObject(); - static const QString WP_OAUTH2_SERVER_URL = "authentication_oauth2_url_base"; - - if (!settingsObject.contains(WP_OAUTH2_SERVER_URL)) { - qDebug() << "Cannot log in to domain because an OAuth2 authorization was required but no authorization server was specified."; - return; - } - - _domainLoginAuthProvider = settingsObject[WP_OAUTH2_SERVER_URL].toString(); - _isDomainLogin = true; +void DialogsManager::showDomainLoginDialog(const QString& domain) { + setDomainLogin(true, domain); LoginDialog::showWithSelection(); } +// #######: TODO: Domain version of toggleLoginDialog()? + +// #######: TODO: Domain version of hiadLoginDialog()? + + void DialogsManager::showUpdateDialog() { UpdateDialog::show(); } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index b76ff69386..fa5c589fb4 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -42,7 +42,7 @@ public: void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } void setAddressBarVisible(bool addressBarVisible); bool getIsDomainLogin() { return _isDomainLogin; } - QString getDomainLoginAuthProvider() { return _domainLoginAuthProvider; } + QString getDomainLoginDomain() { return _domainLoginDomain; } public slots: void showAddressBar(); @@ -52,7 +52,7 @@ public slots: void toggleLoginDialog(); void showLoginDialog(); void hideLoginDialog(); - void showDomainLoginDialog(); + void showDomainLoginDialog(const QString& domain); void octreeStatsDetails(); void lodTools(); void hmdTools(bool showTools); @@ -87,8 +87,9 @@ private: bool _dialogCreatedWhileShown { false }; bool _addressBarVisible { false }; + void setDomainLogin(bool isDomainLogin, const QString& domain = ""); bool _isDomainLogin { false }; - QString _domainLoginAuthProvider { "" }; + QString _domainLoginDomain; }; #endif // hifi_DialogsManager_h diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index d64ebdf42a..c7597c5658 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -143,9 +143,9 @@ void LoginDialog::login(const QString& username, const QString& password) const DependencyManager::get()->requestAccessToken(username, password); } -void LoginDialog::loginDomain(const QString& username, const QString& password, const QString& domainAuthProvider) const { - qDebug() << "Attempting to login" << username << "into a domain through" << domainAuthProvider; - DependencyManager::get()->requestAccessToken(username, password, domainAuthProvider); +void LoginDialog::loginDomain(const QString& username, const QString& password) const { + qDebug() << "Attempting to login" << username << "into a domain"; + DependencyManager::get()->requestAccessToken(username, password); } void LoginDialog::loginThroughOculus() { @@ -428,6 +428,6 @@ bool LoginDialog::getDomainLoginRequested() const { return DependencyManager::get()->getIsDomainLogin(); } -QString LoginDialog::getDomainLoginAuthProvider() const { - return DependencyManager::get()->getDomainLoginAuthProvider(); +QString LoginDialog::getDomainLoginDomain() const { + return DependencyManager::get()->getDomainLoginDomain(); } diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 49d5dc8fac..9f4af5debb 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -72,7 +72,7 @@ protected slots: Q_INVOKABLE QString oculusUserID() const; Q_INVOKABLE void login(const QString& username, const QString& password) const; - Q_INVOKABLE void loginDomain(const QString& username, const QString& password, const QString& domainAuthProvider) const; + Q_INVOKABLE void loginDomain(const QString& username, const QString& password) const; Q_INVOKABLE void loginThroughSteam(); Q_INVOKABLE void linkSteam(); Q_INVOKABLE void createAccountFromSteam(QString username = QString()); @@ -85,7 +85,7 @@ protected slots: Q_INVOKABLE bool getLoginDialogPoppedUp() const; Q_INVOKABLE bool getDomainLoginRequested() const; - Q_INVOKABLE QString getDomainLoginAuthProvider() const; + Q_INVOKABLE QString getDomainLoginDomain() const; }; diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 83c0fd28dd..5589defd80 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -508,7 +508,9 @@ bool AccountManager::checkAndSignalForAccessToken() { if (!hasToken) { // emit a signal so somebody can call back to us and request an access token given a username and password - emit authRequired(); + + // Dialog can be hidden immediately after showing if we've just teleported to the domain, unless the signal is delayed. + QTimer::singleShot(500, this, [this] { emit this->authRequired(); }); } return hasToken; diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 524a7c4b0a..6bb868df61 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -11,100 +11,124 @@ #include "DomainAccountManager.h" -#include - #include -#include #include #include #include #include -#include -#include "DomainAccountManager.h" +#include + #include "NetworkingConstants.h" -#include "OAuthAccessToken.h" #include "NetworkLogging.h" -#include "NodeList.h" -#include "udt/PacketHeaders.h" #include "NetworkAccessManager.h" -const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; -const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; +// FIXME: Generalize to other OAuth2 sources for domain login. +const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; + +// ####### TODO: Add storing domain URL and check against it when retrieving values? +// ####### TODO: Add storing _authURL and check against it when retrieving values? Setting::Handle domainAccessToken {"private/domainAccessToken", "" }; Setting::Handle domainAccessRefreshToken {"private/domainAccessToken", "" }; Setting::Handle domainAccessTokenExpiresIn {"private/domainAccessTokenExpiresIn", -1 }; Setting::Handle domainAccessTokenType {"private/domainAccessTokenType", "" }; -QUrl _domainAuthProviderURL; -// FIXME: If you try to authenticate this way on another domain, no one knows what will happen. Probably death. - -DomainAccountManager::DomainAccountManager() { +DomainAccountManager::DomainAccountManager() : + _authURL(), + _username(), + _access_token(), + _refresh_token() +{ connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer); } -void DomainAccountManager::requestAccessToken(const QString& login, const QString& password, const QString& domainAuthProvider) { +void DomainAccountManager::setAuthURL(const QUrl& authURL) { + if (_authURL != authURL) { + _authURL = authURL; - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString()); + + _access_token = ""; + _refresh_token = ""; + + // ####### TODO: Restore and refresh OAuth2 tokens if have them for this domain. + + // ####### TODO: Handle "keep me logged in". + } +} + +void DomainAccountManager::requestAccessToken(const QString& username, const QString& password) { + + _username = username; + _access_token = ""; + _refresh_token = ""; QNetworkRequest request; - request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); - - _domainAuthProviderURL = domainAuthProvider; - _domainAuthProviderURL.setPath("/oauth/token"); - - QByteArray postData; - postData.append("grant_type=password&"); - postData.append("username=" + QUrl::toPercentEncoding(login) + "&"); - postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); - postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); - - request.setUrl(_domainAuthProviderURL); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - QNetworkReply* requestReply = networkAccessManager.post(request, postData); + // miniOrange WordPress API Authentication plugin: + // - Requires "client_id" parameter. + // - Ignores "state" parameter. + QByteArray formData; + formData.append("grant_type=password&"); + formData.append("username=" + QUrl::toPercentEncoding(username) + "&"); + formData.append("password=" + QUrl::toPercentEncoding(password) + "&"); + formData.append("client_id=" + _clientID); + + request.setUrl(_authURL); + + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* requestReply = networkAccessManager.post(request, formData); connect(requestReply, &QNetworkReply::finished, this, &DomainAccountManager::requestAccessTokenFinished); } void DomainAccountManager::requestAccessTokenFinished() { + QNetworkReply* requestReply = reinterpret_cast(sender()); QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); const QJsonObject& rootObject = jsonResponse.object(); - if (!rootObject.contains("error")) { - // construct an OAuthAccessToken from the json object + auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (200 <= httpStatus && httpStatus < 300) { - if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") - || !rootObject.contains("token_type")) { - // TODO: error handling - malformed token response - qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; - } else { - // clear the path from the response URL so we have the right root URL for this access token + // miniOrange plugin provides no scope. + if (rootObject.contains("access_token")) { + // Success. QUrl rootURL = requestReply->url(); rootURL.setPath(""); - - qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(rootURL.toString()); - - setAccessTokenFromJSON(rootObject); - - emit loginComplete(rootURL); + setTokensFromJSON(rootObject, rootURL); + emit loginComplete(); + } else { + // Failure. + qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; + emit loginFailed(); } + } else { - qCDebug(networking) << "Error in response for password grant -" << rootObject["error_description"].toString(); + // Failure. + qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error() + << "-" << rootObject["error"].toString() << rootObject["error_description"].toString(); emit loginFailed(); } } void DomainAccountManager::sendInterfaceAccessTokenToServer() { - // TODO: Send successful packet to the domain-server. + emit newTokens(); } bool DomainAccountManager::accessTokenIsExpired() { + // ####### TODO: accessTokenIsExpired() + return true; + /* return domainAccessTokenExpiresIn.get() != -1 && domainAccessTokenExpiresIn.get() <= QDateTime::currentMSecsSinceEpoch(); + */ } @@ -115,7 +139,7 @@ bool DomainAccountManager::hasValidAccessToken() { if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qCDebug(networking) << "An access token is required for requests to" - << qPrintable(_domainAuthProviderURL.toString()); + << qPrintable(_authURL.toString()); } return false; @@ -132,21 +156,38 @@ bool DomainAccountManager::hasValidAccessToken() { } -void DomainAccountManager::setAccessTokenFromJSON(const QJsonObject& jsonObject) { +void DomainAccountManager::setTokensFromJSON(const QJsonObject& jsonObject, const QUrl& url) { + _access_token = jsonObject["access_token"].toString(); + _refresh_token = jsonObject["refresh_token"].toString(); + + // ####### TODO: Enable and use these. + // ####### TODO: Protect these per AccountManager? + // ######: TODO: clientID needed? + /* + qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(url.toString()); domainAccessToken.set(jsonObject["access_token"].toString()); domainAccessRefreshToken.set(jsonObject["refresh_token"].toString()); domainAccessTokenExpiresIn.set(QDateTime::currentMSecsSinceEpoch() + (jsonObject["expires_in"].toDouble() * 1000)); domainAccessTokenType.set(jsonObject["token_type"].toString()); + */ } bool DomainAccountManager::checkAndSignalForAccessToken() { bool hasToken = hasValidAccessToken(); + // ####### TODO: Handle hasToken == true. + // It causes the login dialog not to display (OK) but somewhere the domain server needs to be sent it (and if domain server + // gets error when trying to use it then user should be prompted to login). + hasToken = false; + if (!hasToken) { // Emit a signal so somebody can call back to us and request an access token given a user name and password. // Dialog can be hidden immediately after showing if we've just teleported to the domain, unless the signal is delayed. - QTimer::singleShot(500, this, [this] { emit this->authRequired(); }); + auto domain = _authURL.host(); + QTimer::singleShot(500, this, [this, domain] { + emit this->authRequired(domain); + }); } return hasToken; diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 4bb197175e..0388ba1f5a 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -13,6 +13,7 @@ #define hifi_DomainAccountManager_h #include +#include #include @@ -22,25 +23,40 @@ class DomainAccountManager : public QObject, public Dependency { public: DomainAccountManager(); + void setAuthURL(const QUrl& authURL); + void setClientID(const QString& clientID) { _clientID = clientID; } + + QString getUsername() { return _username; } + QString getAccessToken() { return _access_token; } + QString getRefreshToken() { return _refresh_token; } + Q_INVOKABLE bool checkAndSignalForAccessToken(); public slots: - void requestAccessToken(const QString& login, const QString& password, const QString& domainAuthProvider); + void requestAccessToken(const QString& username, const QString& password); void requestAccessTokenFinished(); + signals: - void authRequired(); - void loginComplete(const QUrl& authURL); + void authRequired(const QString& domain); + void loginComplete(); void loginFailed(); void logoutComplete(); + void newTokens(); private slots: private: bool hasValidAccessToken(); bool accessTokenIsExpired(); - void setAccessTokenFromJSON(const QJsonObject&); + void setTokensFromJSON(const QJsonObject&, const QUrl& url); void sendInterfaceAccessTokenToServer(); + + QUrl _authURL; + QString _clientID; + QString _username; // ####### TODO: Store elsewhere? + QString _access_token; // ####... "" + QString _refresh_token; // ####... "" }; #endif // hifi_DomainAccountManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 66d4c58a34..c1a24748f4 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -550,7 +550,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); + if (!extraInfo.isEmpty()) { + auto extraInfoComponents = extraInfo.split("|"); + accountManager->setAuthURL(extraInfoComponents.value(0)); + accountManager->setClientID(extraInfoComponents.value(1)); + } if (!_hasCheckedForDomainAccessToken) { accountManager->checkAndSignalForAccessToken(); diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index ff252426a9..d0bd40f7a7 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -125,6 +125,7 @@ public: bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); + bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; } bool getInterstitialModeEnabled() const; void setInterstitialModeEnabled(bool enableInterstitialMode); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 262ad0d2a4..e02a8dd56e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -34,6 +34,7 @@ #include "AddressManager.h" #include "Assignment.h" #include "AudioHelpers.h" +#include "DomainAccountManager.h" #include "HifiSockAddr.h" #include "FingerprintUtils.h" @@ -104,6 +105,13 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) // clear our NodeList when logout is requested connect(accountManager.data(), &AccountManager::logoutComplete , this, [this]{ reset("Logged out"); }); + // Only used in Interface. + auto domainAccountManager = DependencyManager::get(); + if (domainAccountManager) { + _hasDomainAccountManager = true; + connect(domainAccountManager.data(), &DomainAccountManager::newTokens, this, &NodeList::sendDomainServerCheckIn); + } + // anytime we get a new node we will want to attempt to punch to it connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); connect(this, &LimitedNodeList::nodeSocketUpdated, this, &NodeList::startNodeHolePunch); @@ -468,6 +476,7 @@ void NodeList::sendDomainServerCheckIn() { packetStream << _ownerType.load() << publicSockAddr << localSockAddr << _nodeTypesOfInterest.toList(); packetStream << DependencyManager::get()->getPlaceName(); + // ####### TODO: Also send if need to send new domainLogin data? if (!domainIsConnected) { DataServerAccountInfo& accountInfo = accountManager->getAccountInfo(); packetStream << accountInfo.getUsername(); @@ -477,20 +486,21 @@ void NodeList::sendDomainServerCheckIn() { const QByteArray& usernameSignature = accountManager->getAccountInfo().getUsernameSignature(connectionToken); packetStream << usernameSignature; } else { - packetStream << QString(""); // Placeholder in case have domainUsername. + // ####### TODO: Only append if are going to send domain username? + packetStream << QString(""); // Placeholder in case have domain username. } } else { - packetStream << QString(""); // Placeholder in case have domainUsername. + // ####### TODO: Only append if are going to send domainUsername? + packetStream << QString("") << QString(""); // Placeholders in case have domain username. } - // ####### TODO: Send domain username and signature if domain has these and aren't logged in. - // ####### If get into difficulties, could perhaps send domain's username and signature instead of metaverse. - bool domainLoginIsConnected = false; - if (!domainLoginIsConnected) { - if (false) { // ####### For testing, false causes user to be considered "not logged in". - packetStream << QString("a@b.c"); - if (true) { // ####### For testing, false is unhandled at this stage. - packetStream << QString("signature"); // #######: Consider "logged in" if this is sent during testing. + // Send domain domain login data from Interface to domain server. + if (_hasDomainAccountManager) { + auto domainAccountManager = DependencyManager::get(); + if (!domainAccountManager->getUsername().isEmpty()) { + packetStream << domainAccountManager->getUsername(); + if (!domainAccountManager->getAccessToken().isEmpty()) { + packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken()); } } } diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index c377ea89cb..4954c53c84 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -196,6 +196,8 @@ private: #if (PR_BUILD || DEV_BUILD) bool _shouldSendNewerVersion { false }; #endif + + bool _hasDomainAccountManager { false }; }; #endif // hifi_NodeList_h diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index 6658b7ea12..82c008feef 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -54,8 +54,6 @@ public: void setVerifiedDomainUserName(QString userName) { _verifiedDomainUserName = userName.toLower(); } const QString& getVerifiedDomainUserName() const { return _verifiedDomainUserName; } - void setVerifiedDomainUserGroups(QStringList userGroups) { _verifiedDomainUserGroups = userGroups; } - const QStringList& getVerifiedDomainUserGroups() const { return _verifiedDomainUserGroups; } void setGroupID(QUuid groupID) { _groupID = groupID; if (!groupID.isNull()) { _groupIDSet = true; }} QUuid getGroupID() const { return _groupID; } @@ -106,7 +104,6 @@ protected: QUuid _rankID { QUuid() }; // 0 unless this is for a group QString _verifiedUserName; QString _verifiedDomainUserName; - QStringList _verifiedDomainUserGroups; bool _groupIDSet { false }; QUuid _groupID;