diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 3ae92651b81..76924d81654 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -3,7 +3,7 @@ "settings": [ { "name": "metaverse", - "label": "Metaverse / Networking", + "label": "Networking / Metaverse", "settings": [ { "name": "access_token", @@ -57,6 +57,39 @@ } ] }, + { + "name": "authentication", + "label": "Networking / WordPress OAuth2", + "settings": [ + { + "name": "enable_oauth2", + "label": "Enable OAuth2 Authentication", + "help": "Allow a WordPress-based (miniOrange) OAuth2 service to assign users to groups based on their role with the service.", + "default": false, + "type": "checkbox", + "advanced": true + }, + { + "name": "oauth2_url_path", + "label": "Authentication URL", + "help": "The URL that the Interface will use to login via OAuth2.", + "advanced": true + }, + { + "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 + } + ] + }, { "label": "Monitoring", "name": "monitoring", @@ -381,6 +414,7 @@ "name": "group_permissions", "type": "table", "caption": "Permissions for Users in Groups", + "help": "For groups that are provided from WordPress you need to denote them by putting an \"@\" symbol in front of each item, e.g., \"@silver\".", "categorize_by_key": "permissions_id", "can_add_new_categories": true, "can_add_new_rows": false, @@ -509,6 +543,7 @@ "name": "group_forbiddens", "type": "table", "caption": "Permissions Denied to Users in Groups", + "help": "For groups that are provided from WordPress you need to denote them by putting an \"@\" symbol in front of each item, e.g., \"@silver\".", "categorize_by_key": "permissions_id", "can_add_new_categories": true, "can_add_new_rows": false, diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 09a04464685..ead4002334a 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -4,6 +4,7 @@ // // Created by Stephen Birarda on 2015-08-24. // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -94,6 +95,9 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetBytesLeftToRead() > 0) { // read username from packet packetStream >> username; @@ -101,10 +105,25 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetBytesLeftToRead() > 0) { // read user signature from packet packetStream >> usernameSignature; + + if (message->getBytesLeftToRead() > 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 tokens from packet. + + QString domainTokensString; + packetStream >> domainTokensString; + domainTokens = domainTokensString.split(":"); + } + } } } - node = processAgentConnectRequest(nodeConnection, username, usernameSignature); + node = processAgentConnectRequest(nodeConnection, username, usernameSignature, + domainUsername, domainTokens.value(0), domainTokens.value(1)); } if (node) { @@ -142,7 +161,8 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer_settingsManager.getDomainServerGroupNames() + .filter(QRegularExpression("^(.*[\\s,])?" + QRegularExpression::escape(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 + } + } + } + if (verifiedUsername.isEmpty()) { userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous); #ifdef WANT_DEBUG @@ -256,6 +297,27 @@ 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) { + // A domain group is signified by a leading special character, "@". + // Multiple domain groups may be specified in one domain server setting as a comma- and/or space-separated lists of + // domain group names. For example, "@silver @Gold, @platinum". + auto domainGroups = _server->_settingsManager.getDomainServerBlacklistGroupNames() + .filter(QRegularExpression("^(.*[\\s,])?" + QRegularExpression::escape(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 @@ -275,6 +337,7 @@ void DomainGatekeeper::updateNodePermissions() { // the id and the username in NodePermissions will often be the same, but id is set before // authentication and verifiedUsername is only set once they user's key has been confirmed. QString verifiedUsername = node->getPermissions().getVerifiedUserName(); + QString verifiedDomainUserName = node->getPermissions().getVerifiedDomainUserName(); NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0)); if (node->getPermissions().isAssignment) { @@ -309,7 +372,8 @@ void DomainGatekeeper::updateNodePermissions() { sendingAddress == QHostAddress::LocalHost); } - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress, machineFingerprint); + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUserName, + connectingAddr.getAddress(), hardwareAddress, machineFingerprint); } node->setPermissions(userPerms); @@ -387,12 +451,19 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo return newNode; } +const QString AUTHENTICATION_ENABLE_OAUTH2 = "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"; SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, - const QByteArray& usernameSignature) { + const QByteArray& usernameSignature, + const QString& domainUsername, + const QString& domainAccessToken, + const QString& domainRefreshToken) { auto limitedNodeList = DependencyManager::get(); @@ -419,7 +490,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect #ifdef WANT_DEBUG qDebug() << "stalling login because we have no username-signature:" << username; #endif - return SharedNodePointer(); + if (!domainHasLogin() || domainUsername.isEmpty()) { + return SharedNodePointer(); + } } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { // they sent us a username and the signature verifies it getGroupMemberships(username); @@ -429,17 +502,71 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect requestUserPublicKey(username); #ifdef WANT_DEBUG qDebug() << "stalling login because signature verification failed:" << username; +#endif + if (!domainHasLogin() || domainUsername.isEmpty()) { + return SharedNodePointer(); + } + } + } + + // The domain may have its own users and groups. + QString verifiedDomainUsername; + QStringList verifiedDomainUserGroups; + if (domainHasLogin() && !domainUsername.isEmpty()) { + + if (domainAccessToken.isEmpty()) { + // User is attempting to prove their domain identity. +#ifdef WANT_DEBUG + 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, nodeConnection.senderSockAddr.getAddress(), - nodeConnection.hardwareAddress, nodeConnection.machineFingerprint); + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUsername, + nodeConnection.senderSockAddr.getAddress(), nodeConnection.hardwareAddress, + nodeConnection.machineFingerprint); if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { - sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", - nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorized); + if (domainHasLogin()) { + 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 metaverse permissions to connect to this domain.", + nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedMetaverse); + } #ifdef WANT_DEBUG qDebug() << "stalling login due to permissions:" << username; #endif @@ -600,15 +727,15 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, return true; } else { - // we only send back a LoginError if this wasn't an "optimistic" key + // we only send back a LoginErrorMetaverse if this wasn't an "optimistic" key // (a key that we hoped would work but is probably stale) if (!senderSockAddr.isNull() && !isOptimisticKey) { - qDebug() << "Error decrypting username signature for" << username << "- denying connection."; + qDebug() << "Error decrypting metaverse username signature for" << username << "- denying connection."; sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr, - DomainHandler::ConnectionRefusedReason::LoginError); + DomainHandler::ConnectionRefusedReason::LoginErrorMetaverse); } else if (!senderSockAddr.isNull()) { - qDebug() << "Error decrypting username signature for" << username << "with optimisitic key -" + qDebug() << "Error decrypting metaverse username signature for" << username << "with optimistic key -" << "re-requesting public key and delaying connection"; } @@ -622,7 +749,7 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, if (!senderSockAddr.isNull()) { qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection."; sendConnectionDeniedPacket("Couldn't convert data to RSA key.", senderSockAddr, - DomainHandler::ConnectionRefusedReason::LoginError); + DomainHandler::ConnectionRefusedReason::LoginErrorMetaverse); } } } else { @@ -635,6 +762,25 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, return false; } + +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 verifying domain user.", senderSockAddr, + DomainHandler::ConnectionRefusedReason::LoginErrorDomain); + return false; +} + bool DomainGatekeeper::isWithinMaxCapacity() { // find out what our maximum capacity is QVariant maximumUserCapacityVariant = @@ -907,7 +1053,6 @@ void DomainGatekeeper::getGroupMemberships(const QString& username) { AccountManagerAuth::Required, QNetworkAccessManager::PostOperation, callbackParams, QJsonDocument(json).toJson()); - } QString extractUsernameFromGroupMembershipsReply(QNetworkReply* requestReply) { @@ -962,6 +1107,7 @@ void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply _inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply)); } + void DomainGatekeeper::getDomainOwnerFriendsList() { JSONCallbackParameters callbackParams; callbackParams.callbackReceiver = this; @@ -1010,6 +1156,7 @@ void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply* req qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply->error(); } +// ####### TODO: Domain equivalent or addition void DomainGatekeeper::refreshGroupsCache() { // if agents are connected to this domain, refresh our cached information about groups and memberships in such. getDomainOwnerFriendsList(); @@ -1029,7 +1176,7 @@ void DomainGatekeeper::refreshGroupsCache() { updateNodePermissions(); -#if WANT_DEBUG +#ifdef WANT_DEBUG _server->_settingsManager.debugDumpGroupsState(); #endif } @@ -1061,3 +1208,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. + return _server->_settingsManager.valueForKeyPath(AUTHENTICATION_ENABLE_OAUTH2).toBool() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH).toString().isEmpty() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString().isEmpty() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_PLUGIN_CLIENT_ID).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 adding 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 92b400882ea..cb42baa7e39 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -4,6 +4,7 @@ // // Created by Stephen Birarda on 2015-08-24. // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -29,6 +30,8 @@ #include "NodeConnectionData.h" #include "PendingAssignedNodeData.h" +const QString DOMAIN_GROUP_CHAR = "@"; + class DomainServer; class DomainGatekeeper : public QObject { @@ -71,16 +74,28 @@ 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); SharedNodePointer processAgentConnectRequest(const NodeConnectionData& nodeConnection, const QString& username, - const QByteArray& usernameSignature); + const QByteArray& usernameSignature, + const QString& domainUsername, + const QString& domainAccessToken, + const QString& domainRefreshToken); SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection); bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, 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(); bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature, @@ -120,8 +135,9 @@ private slots: QSet _domainOwnerFriends; // keep track of friends of the domain owner QSet _inFlightGroupMembershipsRequests; // keep track of which we've already asked for - NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress, - const QString& hardwareAddress, const QUuid& machineFingerprint); + NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, QString verifiedDomainUsername, + const QHostAddress& senderAddress, const QString& hardwareAddress, + const QUuid& machineFingerprint); void getGroupMemberships(const QString& username); // void getIsGroupMember(const QString& username, const QUuid groupID); @@ -133,9 +149,18 @@ private slots: 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 73d78a5c708..95bf4adc652 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.startsWith(DOMAIN_GROUP_CHAR)) { + // Ignore domain groups. (Assumption: metaverse group names can't start with a "@".) + 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::getDomainServerGroupNames() { + // All names as listed in the domain server settings; both metaverse groups and domain groups + QSet result; + foreach(NodePermissionsKey groupKey, _groupPermissions.keys()) { + result += _groupPermissions[groupKey]->getID(); + } + return result.toList(); +} + +QStringList DomainServerSettingsManager::getDomainServerBlacklistGroupNames() { + // All names as listed in the domain server settings; 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 e28b9f6cd14..294e441ba4c 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -19,11 +19,11 @@ #include #include - +#include #include -#include "NodePermissions.h" -#include +#include "DomainGatekeeper.h" +#include "NodePermissions.h" const QString SETTINGS_PATHS_KEY = "paths"; @@ -105,6 +105,9 @@ class DomainServerSettingsManager : public QObject { QList getGroupIDs(); QList getBlacklistGroupIDs(); + QStringList getDomainServerGroupNames(); + QStringList getDomainServerBlacklistGroupNames(); + // 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 571b7e074c2..694fd6158f7 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -45,6 +45,9 @@ Item { property bool lostFocus: false 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 domainLoginDomain: loginDialog.getDomainLoginDomain() QtObject { id: d @@ -71,7 +74,12 @@ Item { } function login() { - loginDialog.login(emailField.text, passwordField.text); + if (!isLoggingInToDomain) { + loginDialog.login(emailField.text, passwordField.text); + } else { + loginDialog.loginDomain(emailField.text, passwordField.text); + } + if (linkAccountBody.loginDialogPoppedUp) { var data; if (linkAccountBody.linkSteam) { @@ -87,7 +95,7 @@ Item { } bodyLoader.setSource("LoggingInBody.qml", { "loginDialog": loginDialog, "root": root, "bodyLoader": bodyLoader, "withSteam": linkAccountBody.withSteam, "withOculus": linkAccountBody.withOculus, "linkSteam": linkAccountBody.linkSteam, "linkOculus": linkAccountBody.linkOculus, - "displayName":displayNameField.text }); + "displayName":displayNameField.text, "isLoggingInToDomain": linkAccountBody.isLoggingInToDomain, "domainLoginDomain": linkAccountBody.domainLoginDomain }); } function init() { @@ -98,14 +106,22 @@ Item { loginErrorMessage.wrapMode = Text.WordWrap; errorContainer.height = (loginErrorMessageTextMetrics.width / displayNameField.width) * loginErrorMessageTextMetrics.height; } + var domainLoginText = "Log In to Domain\n" + domainLoginDomain; + loginDialogText.text = (!isLoggingInToDomain) ? "Log In to Metaverse" : domainLoginText; loginButton.text = (!linkAccountBody.linkSteam && !linkAccountBody.linkOculus) ? "Log In" : "Link Account"; + loginButton.text = (!isLoggingInToDomain) ? "Log In to Metaverse" : "Log In to Domain"; loginButton.color = hifi.buttons.blue; displayNameField.placeholderText = "Display Name (optional)"; var savedDisplayName = Settings.getValue("Avatar/displayName", ""); displayNameField.text = savedDisplayName; - emailField.placeholderText = "Username or Email"; - var savedUsername = Settings.getValue("keepMeLoggedIn/savedUsername", ""); - emailField.text = keepMeLoggedInCheckbox.checked ? savedUsername === "Unknown user" ? "" : savedUsername : ""; + emailField.placeholderText = (!isLoggingInToDomain) ? "Username or Email" : "Username"; + if (!isLoggingInToDomain) { + var savedUsername = Settings.getValue("keepMeLoggedIn/savedUsername", ""); + emailField.text = keepMeLoggedInCheckbox.checked ? savedUsername === "Unknown user" ? "" : savedUsername : ""; + } else { + // ####### TODO + } + if (linkAccountBody.linkSteam || linkAccountBody.linkOculus) { loginButton.width = (passwordField.width - hifi.dimensions.contentSpacing.x) / 2; loginButton.anchors.right = displayNameField.right; @@ -131,7 +147,7 @@ Item { Item { id: loginContainer width: displayNameField.width - height: errorContainer.height + displayNameField.height + emailField.height + passwordField.height + 5.5 * hifi.dimensions.contentSpacing.y + + height: errorContainer.height + loginDialogTextContainer.height + displayNameField.height + emailField.height + passwordField.height + 5.5 * hifi.dimensions.contentSpacing.y + keepMeLoggedInCheckbox.height + loginButton.height + cantAccessTextMetrics.height + continueButton.height anchors { top: parent.top @@ -145,9 +161,10 @@ Item { width: parent.width height: loginErrorMessageTextMetrics.height anchors { - bottom: displayNameField.top; - bottomMargin: hifi.dimensions.contentSpacing.y; - left: displayNameField.left; + bottom: loginDialogTextContainer.top + bottomMargin: hifi.dimensions.contentSpacing.y + left: loginDialogTextContainer.left + right: loginDialogTextContainer.right } TextMetrics { id: loginErrorMessageTextMetrics @@ -160,12 +177,45 @@ Item { font.family: linkAccountBody.fontFamily font.pixelSize: linkAccountBody.textFieldFontSize font.bold: linkAccountBody.fontBold + anchors { + top: parent.top + left: parent.left + right: parent.right + } verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter text: "" visible: false } } + + Item { + id: loginDialogTextContainer + height: 56 + anchors { + top: parent.top + left: parent.left + right: parent.right + topMargin: 1.5 * hifi.dimensions.contentSpacing.y + } + + Text { + id: loginDialogText + text: qsTr("Log In") + lineHeight: 1 + color: "white" + anchors { + top: parent.top + left: parent.left + right: parent.right + } + font.family: linkAccountBody.fontFamily + font.pixelSize: 24 + font.bold: linkAccountBody.fontBold + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } HifiControlsUit.TextField { id: displayNameField @@ -174,8 +224,8 @@ Item { font.pixelSize: linkAccountBody.textFieldFontSize styleRenderType: Text.QtRendering anchors { - top: parent.top - topMargin: errorContainer.height + top: loginDialogTextContainer.bottom + topMargin: 1.5 * hifi.dimensions.contentSpacing.y } placeholderText: "Display Name (optional)" activeFocusOnPress: true @@ -193,7 +243,11 @@ Item { case Qt.Key_Return: event.accepted = true; if (keepMeLoggedInCheckbox.checked) { - Settings.setValue("keepMeLoggedIn/savedUsername", emailField.text); + if (!isLoggingInToDomain) { + Settings.setValue("keepMeLoggedIn/savedUsername", emailField.text); + } else { + // ####### TODO + } } linkAccountBody.login(); break; @@ -232,7 +286,11 @@ Item { case Qt.Key_Return: event.accepted = true; if (keepMeLoggedInCheckbox.checked) { - Settings.setValue("keepMeLoggedIn/savedUsername", emailField.text); + if (!isLoggingInToDomain) { + Settings.setValue("keepMeLoggedIn/savedUsername", emailField.text); + } else { + // ####### TODO + } } linkAccountBody.login(); break; @@ -312,7 +370,11 @@ Item { case Qt.Key_Return: event.accepted = true; if (keepMeLoggedInCheckbox.checked) { - Settings.setValue("keepMeLoggedIn/savedUsername", emailField.text); + if (!isLoggingInToDomain) { + Settings.setValue("keepMeLoggedIn/savedUsername", emailField.text); + } else { + // ####### TODO + } } linkAccountBody.login(); break; @@ -321,12 +383,13 @@ Item { } HifiControlsUit.CheckBox { id: keepMeLoggedInCheckbox - checked: Settings.getValue("keepMeLoggedIn", false); + checked: !isLoggingInToDomain ? Settings.getValue("keepMeLoggedIn", false) : false; // ####### TODO text: qsTr("Keep Me Logged In"); boxSize: 18; labelFontFamily: linkAccountBody.fontFamily labelFontSize: 18; color: hifi.colors.white; + visible: !isLoggingInToDomain anchors { top: passwordField.bottom; topMargin: hifi.dimensions.contentSpacing.y; @@ -334,14 +397,22 @@ Item { } onCheckedChanged: { Settings.setValue("keepMeLoggedIn", checked); - if (keepMeLoggedInCheckbox.checked) { - Settings.setValue("keepMeLoggedIn/savedUsername", emailField.text); + if (!isLoggingInToDomain) { + if (keepMeLoggedInCheckbox.checked) { + Settings.setValue("keepMeLoggedIn/savedUsername", emailField.text); + } else { + Settings.setValue("keepMeLoggedIn/savedUsername", ""); + } } else { - Settings.setValue("keepMeLoggedIn/savedUsername", ""); + // ####### TODO } } Component.onCompleted: { - keepMeLoggedInCheckbox.checked = !Account.loggedIn; + if (!isLoggingInToDomain) { + keepMeLoggedInCheckbox.checked = !Account.loggedIn; + } else { + // ####### TODO + } } } HifiControlsUit.Button { @@ -393,7 +464,7 @@ Item { HifiStylesUit.ShortcutText { id: cantAccessText z: 10 - visible: !linkAccountBody.linkSteam && !linkAccountBody.linkOculus + visible: !linkAccountBody.linkSteam && !linkAccountBody.linkOculus && !linkAccountBody.isLoggingInToDomain anchors { top: loginButton.bottom topMargin: hifi.dimensions.contentSpacing.y @@ -403,7 +474,7 @@ Item { font.pixelSize: linkAccountBody.textFieldFontSize font.bold: linkAccountBody.fontBold - text: " Can't access your account?" + text: " Can't access your account?" verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter @@ -492,7 +563,7 @@ Item { id: signUpContainer width: loginContainer.width height: signUpTextMetrics.height - visible: !linkAccountBody.linkSteam && !linkAccountBody.linkOculus + visible: !linkAccountBody.linkSteam && !linkAccountBody.linkOculus && !linkAccountBody.isLoggingInToDomain anchors { left: loginContainer.left top: loginContainer.bottom @@ -529,7 +600,7 @@ Item { leftMargin: hifi.dimensions.contentSpacing.x } - text: "Sign Up" + text: "Sign Up" linkColor: hifi.colors.blueAccent onLinkActivated: { diff --git a/interface/resources/qml/LoginDialog/LoggingInBody.qml b/interface/resources/qml/LoginDialog/LoggingInBody.qml index a0029dc40b5..757c8b7449e 100644 --- a/interface/resources/qml/LoginDialog/LoggingInBody.qml +++ b/interface/resources/qml/LoginDialog/LoggingInBody.qml @@ -3,6 +3,7 @@ // // Created by Wayne Chen on 10/18/18 // Copyright 2018 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -31,6 +32,8 @@ Item { property bool linkSteam: linkSteam property bool linkOculus: linkOculus property bool createOculus: createOculus + property bool isLoggingInToDomain: isLoggingInToDomain + property string domainLoginDomain: domainLoginDomain property string displayName: "" readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() @@ -106,6 +109,9 @@ Item { loggingInGlyph.visible = true; loggingInText.text = "Logging in to Oculus"; loggingInText.x = loggingInHeader.width/2 - loggingInTextMetrics.width/2 + loggingInGlyphTextMetrics.width/2; + } else if (loggingInBody.isLoggingInToDomain) { + loggingInText.text = "Logging in to " + domainLoginDomain; + loggingInText.anchors.centerIn = loggingInHeader; } else { loggingInText.text = "Logging in"; loggingInText.anchors.centerIn = loggingInHeader; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index a5cb1c29369..cc2aed7f53d 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4,6 +4,7 @@ // // Created by Andrzej Kapolka on 5/10/13. // Copyright 2013 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -65,6 +66,7 @@ #include #include #include +#include #include #include #include @@ -852,6 +854,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { #else DependencyManager::set(true, std::bind(&Application::getUserAgent, qApp)); #endif + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(ScriptEngine::CLIENT_SCRIPT, defaultScriptsOverrideOption); DependencyManager::set(); @@ -1348,6 +1351,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo #endif connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); + auto domainAccountManager = DependencyManager::get(); + connect(domainAccountManager.data(), &DomainAccountManager::authRequired, dialogsManager.data(), + &DialogsManager::showDomainLoginDialog); + connect(domainAccountManager.data(), &DomainAccountManager::loginComplete, this, + &Application::updateWindowTitle); + // ####### TODO: Connect any other signals from domainAccountManager. + // use our MyAvatar position and quat for address manager path addressManager->setPositionGetter([] { auto avatarManager = DependencyManager::get(); @@ -1571,7 +1581,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); - if (index != -1) { + if (index != -1 || _disableLoginScreen) { resumeAfterLoginDialogActionTaken(); return; } @@ -2801,6 +2811,7 @@ void Application::cleanupBeforeQuit() { if (!keepMeLoggedIn) { DependencyManager::get()->removeAccountFromFile(); } + // ####### TODO _displayPlugin.reset(); PluginManager::getInstance()->shutdown(); @@ -3150,6 +3161,7 @@ extern void setupPreferences(); static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active = false); #endif +// ####### TODO void Application::showLoginScreen() { #if !defined(DISABLE_QML) auto accountManager = DependencyManager::get(); @@ -7069,19 +7081,23 @@ void Application::updateWindowTitle() const { auto nodeList = DependencyManager::get(); auto accountManager = DependencyManager::get(); + auto domainAccountManager = DependencyManager::get(); auto isInErrorState = nodeList->getDomainHandler().isInErrorState(); + bool isMetaverseLoggedIn = accountManager->isLoggedIn(); + bool isDomainLoggedIn = domainAccountManager->isLoggedIn(); + QString authedDomain = domainAccountManager->getAuthedDomain(); QString buildVersion = " - Vircadia - " + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) + " " + applicationVersion(); - QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; - QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)" : nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)"; - QString username = accountManager->getAccountInfo().getUsername(); - setCrashAnnotation("sentry[user][username]", username.toStdString()); + QString metaverseUsername = accountManager->getAccountInfo().getUsername(); + QString domainUsername = domainAccountManager->getUsername(); + + setCrashAnnotation("sentry[user][username]", metaverseUsername.toStdString()); QString currentPlaceName; if (isServerlessMode()) { @@ -7097,8 +7113,22 @@ void Application::updateWindowTitle() const { } } - QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) - + currentPlaceName + connectionStatus + loginStatus + buildVersion; + QString metaverseDetails; + if (isMetaverseLoggedIn) { + metaverseDetails = "Metaverse: Logged in as " + metaverseUsername; + } else { + metaverseDetails = "Metaverse: Not Logged In"; + } + + QString domainDetails; + if (currentPlaceName == authedDomain && isDomainLoggedIn) { + domainDetails = "Domain: Logged in as " + domainUsername; + } else { + domainDetails = "Domain: Not Logged In"; + } + + QString title = QString() + currentPlaceName + connectionStatus + " (" + metaverseDetails + ") (" + domainDetails + ")" + + buildVersion; #ifndef WIN32 // crashes with vs2013/win32 diff --git a/interface/src/Application.h b/interface/src/Application.h index 3d4e6873a8b..f42696cda00 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -733,6 +733,7 @@ private slots: GraphicsEngine _graphicsEngine; void updateRenderArgs(float deltaTime); + bool _disableLoginScreen { true }; Overlays _overlays; ApplicationOverlay _applicationOverlay; diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index 33d9fddc1b3..070015f05bb 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/Menu.cpp b/interface/src/Menu.cpp index 0ed5f67ea0a..64cdf982393 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -9,6 +9,10 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +// For happ(ier) development of QML, use these two things: +// This forces QML files to be pulled from the source as you edit it: set environment variable HIFI_USE_SOURCE_TREE_RESOURCES=1 +// Use this to live reload: DependencyManager::get()->clearCache(); + #include "Menu.h" #include #include @@ -84,6 +88,13 @@ Menu::Menu() { dialogsManager.data(), &DialogsManager::toggleLoginDialog); } + auto domainLogin = addActionToQMenuAndActionHash(fileMenu, "Domain: Log In"); + connect(domainLogin, &QAction::triggered, [] { + auto dialogsManager = DependencyManager::get(); + dialogsManager->setDomainLoginState(); + dialogsManager->showDomainLoginDialog(); + }); + // File > Quit addActionToQMenuAndActionHash(fileMenu, MenuOption::Quit, Qt::CTRL | Qt::Key_Q, qApp, SLOT(quit()), QAction::QuitRole); diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 0a655de5e53..8c8e3bc3c39 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -4,6 +4,7 @@ // // Created by Clement on 1/18/15. // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -109,11 +110,34 @@ void DialogsManager::setDomainConnectionFailureVisibility(bool visible) { } } +void DialogsManager::setMetaverseLoginState() { + // We're only turning off the domain login trigger but the actual domain auth URL is still saved. + // So we can continue the domain login if desired. + _isDomainLogin = false; +} + +void DialogsManager::setDomainLoginState() { + _isDomainLogin = true; +} + +void DialogsManager::setDomainLogin(bool isDomainLogin, const QString& domain) { + _isDomainLogin = isDomainLogin; + if (!domain.isEmpty()) { + _domainLoginDomain = domain; + } +} + void DialogsManager::toggleLoginDialog() { + setDomainLogin(false); LoginDialog::toggleAction(); } 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. + + setDomainLogin(false); LoginDialog::showWithSelection(); } @@ -121,10 +145,22 @@ void DialogsManager::hideLoginDialog() { LoginDialog::hide(); } + +void DialogsManager::showDomainLoginDialog(const QString& domain) { + setDomainLogin(true, domain); + LoginDialog::showWithSelection(); +} + +// #######: TODO: Domain version of toggleLoginDialog()? + +// #######: TODO: Domain version of hideLoginDialog()? + + void DialogsManager::showUpdateDialog() { UpdateDialog::show(); } + void DialogsManager::octreeStatsDetails() { if (!_octreeStatsDialog) { _octreeStatsDialog = new OctreeStatsDialog(qApp->getWindow(), qApp->getOcteeSceneStats()); diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 949c86c2409..864174296e8 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -4,6 +4,7 @@ // // Created by Clement on 1/18/15. // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -40,6 +41,10 @@ class DialogsManager : public QObject, public Dependency { QPointer getTestingDialog() const { return _testingDialog; } void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } void setAddressBarVisible(bool addressBarVisible); + void setMetaverseLoginState(); + void setDomainLoginState(); + bool getIsDomainLogin() { return _isDomainLogin; } + QString getDomainLoginDomain() { return _domainLoginDomain; } public slots: void showAddressBar(); @@ -49,6 +54,7 @@ public slots: void toggleLoginDialog(); void showLoginDialog(); void hideLoginDialog(); + void showDomainLoginDialog(const QString& domain = ""); void octreeStatsDetails(); void lodTools(); void hmdTools(bool showTools); @@ -82,6 +88,10 @@ private slots: QPointer _domainConnectionDialog; bool _dialogCreatedWhileShown { false }; bool _addressBarVisible { false }; + + void setDomainLogin(bool isDomainLogin, const QString& domain = ""); + bool _isDomainLogin { false }; + QString _domainLoginDomain; }; #endif // hifi_DialogsManager_h diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index c0e96fe8bbf..b45a62ae3ae 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -4,6 +4,7 @@ // // Created by Bradley Austin Davis on 2015/04/14 // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -24,7 +25,9 @@ #include #include "AccountManager.h" +#include "DomainAccountManager.h" #include "DependencyManager.h" +#include "DialogsManager.h" #include "Menu.h" #include "Application.h" @@ -38,12 +41,17 @@ const QUrl LOGIN_DIALOG = PathUtils::qmlUrl("OverlayLoginDialog.qml"); LoginDialog::LoginDialog(QQuickItem *parent) : OffscreenQmlDialog(parent) { auto accountManager = DependencyManager::get(); + auto domainAccountManager = DependencyManager::get(); // the login hasn't been dismissed yet if the user isn't logged in and is encouraged to login. #if !defined(Q_OS_ANDROID) connect(accountManager.data(), &AccountManager::loginComplete, this, &LoginDialog::handleLoginCompleted); connect(accountManager.data(), &AccountManager::loginFailed, this, &LoginDialog::handleLoginFailed); + connect(domainAccountManager.data(), &DomainAccountManager::loginComplete, + this, &LoginDialog::handleLoginCompleted); + connect(domainAccountManager.data(), &DomainAccountManager::loginFailed, + this, &LoginDialog::handleLoginFailed); connect(qApp, &Application::loginDialogFocusEnabled, this, &LoginDialog::focusEnabled); connect(qApp, &Application::loginDialogFocusDisabled, this, &LoginDialog::focusDisabled); connect(this, SIGNAL(dismissedLoginDialog()), qApp, SLOT(onDismissedLoginDialog())); @@ -91,14 +99,16 @@ void LoginDialog::toggleAction() { if (accountManager->isLoggedIn()) { // change the menu item to logout - loginAction->setText("Logout " + accountManager->getAccountInfo().getUsername()); + loginAction->setText("Metaverse: Logout " + accountManager->getAccountInfo().getUsername()); connection = connect(loginAction, &QAction::triggered, accountManager.data(), &AccountManager::logout); } else { // change the menu item to login - loginAction->setText("Log In / Sign Up"); + loginAction->setText("Metaverse: Log In / Sign Up"); connection = connect(loginAction, &QAction::triggered, [] { // if not in login state, show. if (!qApp->getLoginDialogPoppedUp()) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->setMetaverseLoginState(); LoginDialog::showWithSelection(); } }); @@ -131,10 +141,15 @@ void LoginDialog::dismissLoginDialog() { } void LoginDialog::login(const QString& username, const QString& password) const { - qDebug() << "Attempting to login " << username; + qDebug() << "Attempting to login" << username; DependencyManager::get()->requestAccessToken(username, password); } +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() { qDebug() << "Attempting to login through Oculus"; if (auto oculusPlatformPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { @@ -410,3 +425,11 @@ void LoginDialog::signupFailed(QNetworkReply* reply) { emit handleSignupFailed(DEFAULT_SIGN_UP_FAILURE_MESSAGE); } } + +bool LoginDialog::getDomainLoginRequested() const { + return DependencyManager::get()->getIsDomainLogin(); +} + +QString LoginDialog::getDomainLoginDomain() const { + return DependencyManager::get()->getDomainLoginDomain(); +} diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 7c659a93204..9f4af5debbe 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -4,6 +4,7 @@ // // Created by Bradley Austin Davis on 2015/04/14 // Copyright 2015 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -71,6 +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; Q_INVOKABLE void loginThroughSteam(); Q_INVOKABLE void linkSteam(); Q_INVOKABLE void createAccountFromSteam(QString username = QString()); @@ -81,6 +83,10 @@ protected slots: Q_INVOKABLE void signup(const QString& email, const QString& username, const QString& password); Q_INVOKABLE bool getLoginDialogPoppedUp() const; + + Q_INVOKABLE bool getDomainLoginRequested() const; + Q_INVOKABLE QString getDomainLoginDomain() const; + }; #endif // hifi_LoginDialog_h diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 83c0fd28dd9..5589defd809 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 new file mode 100644 index 00000000000..928b581a5b3 --- /dev/null +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -0,0 +1,203 @@ +// +// DomainAccountManager.cpp +// libraries/networking/src +// +// Created by David Rowe on 23 Jul 2020. +// Copyright 2020 Vircadia contributors. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "DomainAccountManager.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "NetworkingConstants.h" +#include "NetworkAccessManager.h" +#include "NetworkLogging.h" +#include "NodeList.h" + +// FIXME: Generalize to other OAuth2 sources for domain login. + +const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; + +// ####### TODO: Enable and use these? +// ####### 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", "" }; +*/ + +DomainAccountManager::DomainAccountManager() : + _authURL(), + _username(), + _access_token(), + _refresh_token(), + _domain_name() +{ + connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer); +} + +void DomainAccountManager::setAuthURL(const QUrl& authURL) { + if (_authURL != authURL) { + _authURL = authURL; + + 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". + } +} + +bool DomainAccountManager::isLoggedIn() { + return !_authURL.isEmpty() && hasValidAccessToken(); +} + +void DomainAccountManager::requestAccessToken(const QString& username, const QString& password) { + + _username = username; + _access_token = ""; + _refresh_token = ""; + + QNetworkRequest request; + + request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + // 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(); + + auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (200 <= httpStatus && httpStatus < 300) { + + // miniOrange plugin provides no scope. + if (rootObject.contains("access_token")) { + // Success. + auto nodeList = DependencyManager::get(); + _domain_name = nodeList->getDomainHandler().getHostname(); + QUrl rootURL = requestReply->url(); + rootURL.setPath(""); + 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 { + // Failure. + qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error() + << "-" << rootObject["error"].toString() << rootObject["error_description"].toString(); + emit loginFailed(); + } +} + +void DomainAccountManager::sendInterfaceAccessTokenToServer() { + emit newTokens(); +} + +bool DomainAccountManager::accessTokenIsExpired() { + // ####### TODO: accessTokenIsExpired() + return true; + /* + return domainAccessTokenExpiresIn.get() != -1 && domainAccessTokenExpiresIn.get() <= QDateTime::currentMSecsSinceEpoch(); + */ +} + + +bool DomainAccountManager::hasValidAccessToken() { + // ###### TODO: wire this up to actually retrieve a token (based on session or storage) and confirm that it is in fact valid and relevant to the current domain. + // QString currentDomainAccessToken = domainAccessToken.get(); + QString currentDomainAccessToken = _access_token; + + // if (currentDomainAccessToken.isEmpty() || accessTokenIsExpired()) { + if (currentDomainAccessToken.isEmpty()) { + if (VERBOSE_HTTP_REQUEST_DEBUGGING) { + qCDebug(networking) << "An access token is required for requests to" + << qPrintable(_authURL.toString()); + } + + return false; + } + + // ####### TODO + + // if (!_isWaitingForTokenRefresh && needsToRefreshToken()) { + // refreshAccessToken(); + // } + + return true; +} + +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. + 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 new file mode 100644 index 00000000000..31226d69901 --- /dev/null +++ b/libraries/networking/src/DomainAccountManager.h @@ -0,0 +1,66 @@ +// +// DomainAccountManager.h +// libraries/networking/src +// +// Created by David Rowe on 23 Jul 2020. +// Copyright 2020 Vircadia contributors. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_DomainAccountManager_h +#define hifi_DomainAccountManager_h + +#include +#include + +#include + + +class DomainAccountManager : public QObject, public Dependency { + Q_OBJECT +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; } + QString getAuthedDomain() { return _domain_name; } + + bool isLoggedIn(); + + Q_INVOKABLE bool checkAndSignalForAccessToken(); + +public slots: + void requestAccessToken(const QString& username, const QString& password); + + void requestAccessTokenFinished(); + +signals: + void authRequired(const QString& domain); + void loginComplete(); + void loginFailed(); + void logoutComplete(); + void newTokens(); + +private slots: + +private: + bool hasValidAccessToken(); + bool accessTokenIsExpired(); + void setTokensFromJSON(const QJsonObject&, const QUrl& url); + void sendInterfaceAccessTokenToServer(); + + QUrl _authURL; + QString _clientID; + QString _username; // ####### TODO: Store elsewhere? + QString _access_token; // ####... "" + QString _refresh_token; // ####... "" + QString _domain_name; // ####... "" +}; + +#endif // hifi_DomainAccountManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index d34b5cf090f..c1a24748f46 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -4,6 +4,7 @@ // // Created by Stephen Birarda on 2/18/2014. // Copyright 2014 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -24,6 +25,7 @@ #include "AddressManager.h" #include "Assignment.h" +#include "DomainAccountManager.h" #include "HifiSockAddr.h" #include "NodeList.h" #include "udt/Packet.h" @@ -144,7 +146,8 @@ void DomainHandler::hardReset(QString reason) { bool DomainHandler::isHardRefusal(int reasonCode) { return (reasonCode == (int)ConnectionRefusedReason::ProtocolMismatch || reasonCode == (int)ConnectionRefusedReason::TooManyUsers || - reasonCode == (int)ConnectionRefusedReason::NotAuthorized || + reasonCode == (int)ConnectionRefusedReason::NotAuthorizedMetaverse || + reasonCode == (int)ConnectionRefusedReason::NotAuthorizedDomain || reasonCode == (int)ConnectionRefusedReason::TimedOut); } @@ -219,6 +222,8 @@ void DomainHandler::setURLAndID(QUrl domainURL, QUuid domainID) { QString previousHost = _domainURL.host(); _domainURL = domainURL; + _hasCheckedForDomainAccessToken = false; + if (previousHost != domainURL.host()) { qCDebug(networking) << "Updated domain hostname to" << domainURL.host(); @@ -489,16 +494,33 @@ void DomainHandler::processICEResponsePacket(QSharedPointer mes } } -bool DomainHandler::reasonSuggestsLogin(ConnectionRefusedReason reasonCode) { +bool DomainHandler::reasonSuggestsMetaverseLogin(ConnectionRefusedReason reasonCode) { + switch (reasonCode) { + case ConnectionRefusedReason::LoginErrorMetaverse: + case ConnectionRefusedReason::NotAuthorizedMetaverse: + return true; + + default: + case ConnectionRefusedReason::Unknown: + case ConnectionRefusedReason::ProtocolMismatch: + case ConnectionRefusedReason::TooManyUsers: + case ConnectionRefusedReason::NotAuthorizedDomain: + return false; + } + return false; +} + +bool DomainHandler::reasonSuggestsDomainLogin(ConnectionRefusedReason reasonCode) { switch (reasonCode) { - case ConnectionRefusedReason::LoginError: - case ConnectionRefusedReason::NotAuthorized: + case ConnectionRefusedReason::LoginErrorDomain: + case ConnectionRefusedReason::NotAuthorizedDomain: return true; default: case ConnectionRefusedReason::Unknown: case ConnectionRefusedReason::ProtocolMismatch: case ConnectionRefusedReason::TooManyUsers: + case ConnectionRefusedReason::NotAuthorizedMetaverse: return false; } return false; @@ -528,7 +550,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); - // Some connection refusal reasons imply that a login is required. If so, suggest a new login - if (reasonSuggestsLogin(reasonCode)) { - qCWarning(networking) << "Make sure you are logged in."; + // Some connection refusal reasons imply that a login is required. If so, suggest a new login. + if (reasonSuggestsMetaverseLogin(reasonCode)) { + qCWarning(networking) << "Make sure you are logged in to the metaverse."; + + auto accountManager = DependencyManager::get(); if (!_hasCheckedForAccessToken) { accountManager->checkAndSignalForAccessToken(); @@ -559,6 +584,23 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointergenerateNewUserKeypair(); _connectionDenialsSinceKeypairRegen = 0; } + } else if (reasonSuggestsDomainLogin(reasonCode)) { + qCWarning(networking) << "Make sure you are logged in to the domain."; + + auto accountManager = DependencyManager::get(); + if (!extraInfo.isEmpty()) { + auto extraInfoComponents = extraInfo.split("|"); + accountManager->setAuthURL(extraInfoComponents.value(0)); + accountManager->setClientID(extraInfoComponents.value(1)); + } + + if (!_hasCheckedForDomainAccessToken) { + accountManager->checkAndSignalForAccessToken(); + _hasCheckedForDomainAccessToken = true; + } + + // ####### TODO: regenerate key-pair after several failed connection attempts, similar to metaverse login code? + } } diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 178c56c34a2..d0bd40f7a7f 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -4,6 +4,7 @@ // // Created by Stephen Birarda on 2/18/2014. // Copyright 2014 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -124,6 +125,7 @@ class DomainHandler : public QObject { 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); @@ -172,14 +174,14 @@ class DomainHandler : public QObject { * The communications protocols of the domain and your Interface are not the same. * * - * LoginError + * LoginErrorMetaverse * 2 - * You could not be logged into the domain. + * You could not be logged into the domain per your metaverse login. * * - * NotAuthorized + * NotAuthorizedMetaverse * 3 - * You are not authorized to connect to the domain. + * You are not authorized to connect to the domain per your metaverse login. * * * TooManyUsers @@ -191,6 +193,16 @@ class DomainHandler : public QObject { * 5 * Connecting to the domain timed out. * + * + * LoginErrorDomain + * 2 + * You could not be logged into the domain per your domain login. + * + * + * NotAuthorizedDomain + * 6 + * You are not authorized to connect to the domain per your domain login. + * * * * @typedef {number} Window.ConnectionRefusedReason @@ -198,10 +210,12 @@ class DomainHandler : public QObject { enum class ConnectionRefusedReason : uint8_t { Unknown, ProtocolMismatch, - LoginError, - NotAuthorized, + LoginErrorMetaverse, + NotAuthorizedMetaverse, TooManyUsers, - TimedOut + TimedOut, + LoginErrorDomain, + NotAuthorizedDomain }; public slots: @@ -247,7 +261,8 @@ private slots: void limitOfSilentDomainCheckInsReached(); private: - bool reasonSuggestsLogin(ConnectionRefusedReason reasonCode); + bool reasonSuggestsMetaverseLogin(ConnectionRefusedReason reasonCode); + bool reasonSuggestsDomainLogin(ConnectionRefusedReason reasonCode); void sendDisconnectPacket(); void hardReset(QString reason); @@ -278,6 +293,7 @@ private slots: QSet _domainConnectionRefusals; bool _hasCheckedForAccessToken { false }; + bool _hasCheckedForDomainAccessToken { false }; int _connectionDenialsSinceKeypairRegen { 0 }; int _checkInPacketsSinceLastReply { 0 }; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 2c584b1c481..e02a8dd56e1 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -4,6 +4,7 @@ // // Created by Stephen Birarda on 2/15/13. // Copyright 2013 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -33,6 +34,7 @@ #include "AddressManager.h" #include "Assignment.h" #include "AudioHelpers.h" +#include "DomainAccountManager.h" #include "HifiSockAddr.h" #include "FingerprintUtils.h" @@ -103,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); @@ -379,6 +388,7 @@ void NodeList::sendDomainServerCheckIn() { if (domainPacketType == PacketType::DomainConnectRequest) { #if (PR_BUILD || DEV_BUILD) + // ####### if (_shouldSendNewerVersion) { domainPacket->setVersion(versionForPacketType(domainPacketType) + 1); } @@ -466,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(); @@ -474,6 +485,23 @@ void NodeList::sendDomainServerCheckIn() { if (requiresUsernameSignature && accountManager->getAccountInfo().hasPrivateKey()) { const QByteArray& usernameSignature = accountManager->getAccountInfo().getUsernameSignature(connectionToken); packetStream << usernameSignature; + } else { + // ####### TODO: Only append if are going to send domain username? + packetStream << QString(""); // Placeholder in case have domain username. + } + } else { + // ####### TODO: Only append if are going to send domainUsername? + packetStream << QString("") << QString(""); // Placeholders in case have domain username. + } + + // 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 c377ea89cb9..4954c53c845 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -196,6 +196,8 @@ private slots: #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 583c1b29ac8..82c008feef0 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -4,6 +4,7 @@ // // Created by Seth Alves on 2016-6-1. // Copyright 2016 High Fidelity, Inc. +// Copyright 2020 Vircadia contributors. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -51,6 +52,9 @@ class NodePermissions { void setVerifiedUserName(QString userName) { _verifiedUserName = userName.toLower(); } const QString& getVerifiedUserName() const { return _verifiedUserName; } + void setVerifiedDomainUserName(QString userName) { _verifiedDomainUserName = userName.toLower(); } + const QString& getVerifiedDomainUserName() const { return _verifiedDomainUserName; } + void setGroupID(QUuid groupID) { _groupID = groupID; if (!groupID.isNull()) { _groupIDSet = true; }} QUuid getGroupID() const { return _groupID; } bool isGroup() const { return _groupIDSet; } @@ -99,6 +103,7 @@ class NodePermissions { QString _id; QUuid _rankID { QUuid() }; // 0 unless this is for a group QString _verifiedUserName; + QString _verifiedDomainUserName; bool _groupIDSet { false }; QUuid _groupID;