From 429b4ceefa870bd8fc400f36fc7151cff2d8ae38 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 28 Jul 2020 21:23:55 +1200 Subject: [PATCH 01/22] Flesh out domain groups code --- domain-server/src/DomainGatekeeper.cpp | 56 ++++++++++++---------- domain-server/src/DomainGatekeeper.h | 7 ++- libraries/networking/src/NodePermissions.h | 3 -- 3 files changed, 36 insertions(+), 30 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 75cb11dab7..3c03583919 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -156,10 +156,8 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer_settingsManager.getAllKnownGroupNames().contains(group)) { - userPerms |= _server->_settingsManager.getPermissionsForGroup(group, QUuid()); + if (!verifiedDomainUserName.isEmpty()) { + auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; + foreach (QString userGroup, userGroups) { + if (_server->_settingsManager.getAllKnownGroupNames().contains(userGroup, Qt::CaseInsensitive)) { + userPerms |= _server->_settingsManager.getPermissionsForGroup(userGroup, QUuid()); // No rank for domain groups. +// ####### Enable ifdef //#ifdef WANT_DEBUG - qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << group << "so:" << userPerms; + qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << userGroup + << "so:" << userPerms; //#endif - } } userPerms.setVerifiedDomainUserName(verifiedDomainUserName); - userPerms.setVerifiedDomainUserGroups(verifiedDomainUserGroups); } if (verifiedUsername.isEmpty()) { @@ -309,7 +308,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 +343,7 @@ void DomainGatekeeper::updateNodePermissions() { } userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUserName, - verifiedDomainUserGroups, connectingAddr.getAddress(), - hardwareAddress, machineFingerprint); + connectingAddr.getAddress(), hardwareAddress, machineFingerprint); } node->setPermissions(userPerms); @@ -486,31 +483,28 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // ####### TODO: OAuth2 corollary of metaverse code, above. + getDomainGroupMemberships(domainUsernameSignature); // Optimistically get started on group memberships. +#ifdef WANT_DEBUG + qDebug() << "stalling login because we have no domain username-signature:" << domainUsername; +#endif 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(" "); - + getDomainGroupMemberships(domainUsername); 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 domain signature verification failed:" << domainUsername; #endif return SharedNodePointer(); } } - userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUsername, verifiedDomainUserGroups, + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, verifiedDomainUsername, nodeConnection.senderSockAddr.getAddress(), nodeConnection.hardwareAddress, nodeConnection.machineFingerprint); @@ -1004,7 +998,6 @@ void DomainGatekeeper::getGroupMemberships(const QString& username) { AccountManagerAuth::Required, QNetworkAccessManager::PostOperation, callbackParams, QJsonDocument(json).toJson()); - } QString extractUsernameFromGroupMembershipsReply(QNetworkReply* requestReply) { @@ -1059,6 +1052,18 @@ void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply _inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply)); } + +void DomainGatekeeper::getDomainGroupMemberships(const QString& domainUserName) { + + // ####### TODO: Get user's domain group memberships (WordPress roles) from domain. + // This may be able to be provided at the same time as the "authenticate user" call to the domain API, in which case + // a copy of some of the following code can be made there. However, this code is still needed for refreshing groups. + + QStringList wordpressGroupsForUser = QStringList("siLVer"); + _domainGroupMemberships[domainUserName] = wordpressGroupsForUser; +} + + void DomainGatekeeper::getDomainOwnerFriendsList() { JSONCallbackParameters callbackParams; callbackParams.callbackReceiver = this; @@ -1107,6 +1112,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(); diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 172c63028c..99bd875457 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -128,14 +128,17 @@ 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(); + // Login and groups for domain, separate from metaverse. bool domainHasLogin(); + void getDomainGroupMemberships(const QString& domainUserName); + QHash _domainGroupMemberships; // // Local ID management. void initLocalIDManagement(); 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; From 94908fd1a7e567cbc1a203a67a7a322decac9685 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 28 Jul 2020 22:05:40 +1200 Subject: [PATCH 02/22] Add support for group lists in domain server --- domain-server/src/DomainGatekeeper.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 3c03583919..e296328e32 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -174,11 +174,16 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin if (!verifiedDomainUserName.isEmpty()) { auto userGroups = _domainGroupMemberships[verifiedDomainUserName]; foreach (QString userGroup, userGroups) { - if (_server->_settingsManager.getAllKnownGroupNames().contains(userGroup, Qt::CaseInsensitive)) { - userPerms |= _server->_settingsManager.getPermissionsForGroup(userGroup, QUuid()); // No rank for domain groups. + // Domain groups may be specified as comma- and/or space-separated lists of group names. + // For example, "silver gold, platinum". + auto domainGroups = _server->_settingsManager.getAllKnownGroupNames() + .filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$", + QRegularExpression::CaseInsensitiveOption)); + foreach(QString domainGroup, domainGroups) { + userPerms |= _server->_settingsManager.getPermissionsForGroup(domainGroup, QUuid()); // No rank for domain groups. // ####### Enable ifdef //#ifdef WANT_DEBUG - qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << userGroup + qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << domainGroup << "so:" << userPerms; //#endif } @@ -1059,7 +1064,8 @@ void DomainGatekeeper::getDomainGroupMemberships(const QString& domainUserName) // This may be able to be provided at the same time as the "authenticate user" call to the domain API, in which case // a copy of some of the following code can be made there. However, this code is still needed for refreshing groups. - QStringList wordpressGroupsForUser = QStringList("siLVer"); + QStringList wordpressGroupsForUser; + wordpressGroupsForUser << "silVER" << "gold"; _domainGroupMemberships[domainUserName] = wordpressGroupsForUser; } From d5e189422f0ac7e3f7845c4e5c530b3aeac52b8f Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 29 Jul 2020 08:08:08 +1200 Subject: [PATCH 03/22] Support blacklisting per domain groups --- domain-server/src/DomainGatekeeper.cpp | 32 +++++++++++++++---- .../src/DomainServerSettingsManager.cpp | 18 +++++++++++ .../src/DomainServerSettingsManager.h | 3 ++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index e296328e32..5944b6242c 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -169,27 +169,25 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin #endif } - // If this user is a known member of an externally-hosted group, give them the implied permissions. + // If this user is a known member of a domain group, give them the implied permissions. // Do before processing verifiedUsername in case user is logged into the metaverse and is a member of a blacklist group. 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.getAllKnownGroupNames() + 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. -// ####### Enable ifdef -//#ifdef WANT_DEBUG +#ifdef WANT_DEBUG qDebug() << "| user-permissions: domain user " << verifiedDomainUserName << "is in group:" << domainGroup << "so:" << userPerms; -//#endif +#endif } } - userPerms.setVerifiedDomainUserName(verifiedDomainUserName); } if (verifiedUsername.isEmpty()) { @@ -293,6 +291,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 @@ -1065,7 +1083,7 @@ void DomainGatekeeper::getDomainGroupMemberships(const QString& domainUserName) // a copy of some of the following code can be made there. However, this code is still needed for refreshing groups. QStringList wordpressGroupsForUser; - wordpressGroupsForUser << "silVER" << "gold"; + wordpressGroupsForUser << "silVER" << "gold" << "coal"; _domainGroupMemberships[domainUserName] = wordpressGroupsForUser; } diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 73d78a5c70..30ca15a51e 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -2185,6 +2185,24 @@ QList DomainServerSettingsManager::getBlacklistGroupIDs() { return result.toList(); } +QStringList DomainServerSettingsManager::getDomainGroupNames() { + // Names as configured in domain server; not necessarily mnetaverse 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..8c18c22b32 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -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); From 7c8993b34f6dc307e207597b8554e3a19bcf408e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 29 Jul 2020 10:03:44 +1200 Subject: [PATCH 04/22] Add TODO --- domain-server/src/DomainGatekeeper.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 5944b6242c..32b02382da 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -1082,6 +1082,8 @@ void DomainGatekeeper::getDomainGroupMemberships(const QString& domainUserName) // This may be able to be provided at the same time as the "authenticate user" call to the domain API, in which case // a copy of some of the following code can be made there. However, this code is still needed for refreshing groups. + // ####### TODO: Check how often this method and the WordPress API is called. + QStringList wordpressGroupsForUser; wordpressGroupsForUser << "silVER" << "gold" << "coal"; _domainGroupMemberships[domainUserName] = wordpressGroupsForUser; From 6b28f3ea0dd1536bd480403bb8c3e3407cd7b99e Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 31 Jul 2020 15:20:13 +1200 Subject: [PATCH 05/22] Reinstate TODOs --- interface/src/ui/DialogsManager.cpp | 5 +++++ interface/src/ui/LoginDialog.cpp | 3 +++ 2 files changed, 8 insertions(+) diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index c61d4c4069..9cad1b8a9e 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -144,6 +144,11 @@ void DialogsManager::showDomainLoginDialog() { LoginDialog::showWithSelection(); } +// #######: TODO: Domain version of toggleLoginDialog()? + +// #######: TODO: Domain version of hiadLoginDialog()? + + void DialogsManager::showUpdateDialog() { UpdateDialog::show(); } diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index d64ebdf42a..cd686719ea 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -146,6 +146,9 @@ void LoginDialog::login(const QString& username, const QString& password) const 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); + + // ####### TODO: It may not be necessary to pass domainAuthProvider to the login dialog and through to here because it was + // originally provided to the QML from C++. } void LoginDialog::loginThroughOculus() { From 56ba137ee3b4b4b81500ce41d89b6d5c07c7b908 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Fri, 31 Jul 2020 20:48:27 +1200 Subject: [PATCH 06/22] Get OAuth2 URL from server settings --- domain-server/resources/describe-settings.json | 2 +- domain-server/src/DomainGatekeeper.cpp | 18 ++++++++++-------- .../qml/LoginDialog/LinkAccountBody.qml | 3 +-- interface/src/ui/DialogsManager.cpp | 10 ---------- interface/src/ui/DialogsManager.h | 2 -- interface/src/ui/LoginDialog.cpp | 13 +++---------- interface/src/ui/LoginDialog.h | 3 +-- .../networking/src/DomainAccountManager.cpp | 14 ++++++++++++-- .../networking/src/DomainAccountManager.h | 7 ++++++- libraries/networking/src/DomainHandler.cpp | 5 ++++- 10 files changed, 38 insertions(+), 39 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 281a0be9cc..14c15e1e3d 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -85,7 +85,7 @@ "backup": false }, { - "name": "authentication_oauth2_url_base", + "name": "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 32b02382da..f41c20e245 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -444,6 +444,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo return newNode; } +const QString AUTHENTICATION_OAUTH2_URL_BASE = "authentication.oauth2_url_base"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; @@ -533,8 +534,14 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { if (domainHasLogin()) { + QString domainAuthURL; + auto domainAuthURLVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_BASE); + if (domainAuthURLVariant.canConvert()) { + domainAuthURL = domainAuthURLVariant.toString(); + qDebug() << "Domain authorization URL:" << domainAuthURL; + } sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", - nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain); + nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, domainAuthURL); } else { sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedMetaverse); @@ -1164,13 +1171,8 @@ void DomainGatekeeper::refreshGroupsCache() { } 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 + // The domain may have its own users and groups. This is enabled in the server settings by ... ####### + // ####### TODO: Base on server settings. return true; } diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 6f437bb991..3d715d1a39 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -46,7 +46,6 @@ Item { readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() readonly property bool isLoggingInToDomain: loginDialog.getDomainLoginRequested() - readonly property string domainAuthProvider: loginDialog.getDomainLoginAuthProvider() QtObject { id: d @@ -76,7 +75,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/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 9cad1b8a9e..848663967f 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" @@ -131,15 +130,6 @@ 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; LoginDialog::showWithSelection(); } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index b76ff69386..30127ced68 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -42,7 +42,6 @@ public: void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } void setAddressBarVisible(bool addressBarVisible); bool getIsDomainLogin() { return _isDomainLogin; } - QString getDomainLoginAuthProvider() { return _domainLoginAuthProvider; } public slots: void showAddressBar(); @@ -88,7 +87,6 @@ private: bool _addressBarVisible { false }; bool _isDomainLogin { false }; - QString _domainLoginAuthProvider { "" }; }; #endif // hifi_DialogsManager_h diff --git a/interface/src/ui/LoginDialog.cpp b/interface/src/ui/LoginDialog.cpp index cd686719ea..3cc37bcadb 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -143,12 +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); - - // ####### TODO: It may not be necessary to pass domainAuthProvider to the login dialog and through to here because it was - // originally provided to the QML from C++. +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() { @@ -430,7 +427,3 @@ void LoginDialog::signupFailed(QNetworkReply* reply) { bool LoginDialog::getDomainLoginRequested() const { return DependencyManager::get()->getIsDomainLogin(); } - -QString LoginDialog::getDomainLoginAuthProvider() const { - return DependencyManager::get()->getDomainLoginAuthProvider(); -} diff --git a/interface/src/ui/LoginDialog.h b/interface/src/ui/LoginDialog.h index 49d5dc8fac..310a5db255 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,6 @@ protected slots: Q_INVOKABLE bool getLoginDialogPoppedUp() const; Q_INVOKABLE bool getDomainLoginRequested() const; - Q_INVOKABLE QString getDomainLoginAuthProvider() const; }; diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 524a7c4b0a..78ae779fca 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -45,7 +45,17 @@ DomainAccountManager::DomainAccountManager() { 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; + + qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString()); + + // ####### TODO: See AccountManager::setAuthURL(). + } +} + +void DomainAccountManager::requestAccessToken(const QString& login, const QString& password) { QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); @@ -53,7 +63,7 @@ void DomainAccountManager::requestAccessToken(const QString& login, const QStrin request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); - _domainAuthProviderURL = domainAuthProvider; + _domainAuthProviderURL = _authURL; _domainAuthProviderURL.setPath("/oauth/token"); QByteArray postData; diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 4bb197175e..eb0f2659dd 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,10 +23,12 @@ class DomainAccountManager : public QObject, public Dependency { public: DomainAccountManager(); + void setAuthURL(const QUrl& authURL); + Q_INVOKABLE bool checkAndSignalForAccessToken(); public slots: - void requestAccessToken(const QString& login, const QString& password, const QString& domainAuthProvider); + void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenFinished(); signals: @@ -41,6 +44,8 @@ private: bool accessTokenIsExpired(); void setAccessTokenFromJSON(const QJsonObject&); void sendInterfaceAccessTokenToServer(); + + QUrl _authURL; }; #endif // hifi_DomainAccountManager_h diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 66d4c58a34..0a61036b96 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -584,8 +584,11 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); + if (!extraInfo.isEmpty()) { + accountManager->setAuthURL(extraInfo); + } if (!_hasCheckedForDomainAccessToken) { accountManager->checkAndSignalForAccessToken(); From aff327520760c7c40bc59f77e5fb18aa31d7b721 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 1 Aug 2020 10:17:27 +1200 Subject: [PATCH 07/22] Initial OAuth2 login working --- .../networking/src/DomainAccountManager.cpp | 72 ++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 78ae779fca..d36f4b5f09 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -29,17 +29,19 @@ #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; +const QString DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE = "foo bar"; // ####### TODO: WordPress plugin's required scope. +// ####### TODO: Should scope be configured in domain server settings? + +// ####### 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() { connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer); @@ -57,25 +59,29 @@ void DomainAccountManager::setAuthURL(const QUrl& authURL) { void DomainAccountManager::requestAccessToken(const QString& login, const QString& password) { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkRequest request; - request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); - - _domainAuthProviderURL = _authURL; - _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"); + // ####### TODO: WordPress plugin's authorization requirements. + request.setRawHeader(QByteArray("Authorization"), QByteArray("Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x")); - QNetworkReply* requestReply = networkAccessManager.post(request, postData); + QByteArray formData; + formData.append("grant_type=password&"); + formData.append("username=" + QUrl::toPercentEncoding(login) + "&"); + formData.append("password=" + QUrl::toPercentEncoding(password) + "&"); + formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); + // ####### TODO: Include state? + + QUrl domainAuthURL = _authURL; + domainAuthURL.setPath("/token"); // ####### TODO: miniOrange-mandated URL. ####### TODO: Should this be included in the server settings value? + request.setUrl(domainAuthURL); + + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* requestReply = networkAccessManager.post(request, formData); connect(requestReply, &QNetworkReply::finished, this, &DomainAccountManager::requestAccessTokenFinished); } @@ -86,19 +92,25 @@ void DomainAccountManager::requestAccessTokenFinished() { const QJsonObject& rootObject = jsonResponse.object(); if (!rootObject.contains("error")) { - // construct an OAuthAccessToken from the json object + // ####### TODO: Process response scope? + // ####### TODO: Process response state? - if (!rootObject.contains("access_token") || !rootObject.contains("expires_in") + if (!rootObject.contains("access_token") + // ####### TODO: Does WordPRess plugin provide "expires_in"? + // If so, handle here, or is it just the domain server that needs to use it? + //|| !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."; + emit loginFailed(); + } else { + // clear the path from the response URL so we have the right root URL for this access token QUrl rootURL = requestReply->url(); rootURL.setPath(""); qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(rootURL.toString()); - setAccessTokenFromJSON(rootObject); emit loginComplete(rootURL); @@ -114,7 +126,11 @@ void DomainAccountManager::sendInterfaceAccessTokenToServer() { } bool DomainAccountManager::accessTokenIsExpired() { + // ####### TODO: accessTokenIsExpired() + return true; + /* return domainAccessTokenExpiresIn.get() != -1 && domainAccessTokenExpiresIn.get() <= QDateTime::currentMSecsSinceEpoch(); + */ } @@ -125,7 +141,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; @@ -143,15 +159,23 @@ bool DomainAccountManager::hasValidAccessToken() { } void DomainAccountManager::setAccessTokenFromJSON(const QJsonObject& jsonObject) { + // ####### TODO: Enable and use these. + /* 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. From 15c6baceb8ed6a21a4a6a1e741b8c854ebecfb26 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 1 Aug 2020 18:57:21 +1200 Subject: [PATCH 08/22] Send OAuth2 username and tokens to domain server --- domain-server/src/DomainGatekeeper.cpp | 21 ++++++------- domain-server/src/DomainGatekeeper.h | 4 +-- .../networking/src/DomainAccountManager.cpp | 22 +++++++++++--- .../networking/src/DomainAccountManager.h | 10 ++++++- libraries/networking/src/NodeList.cpp | 30 ++++++++++++------- libraries/networking/src/NodeList.h | 2 ++ 6 files changed, 61 insertions(+), 28 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index f41c20e245..a1581c26f1 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -95,7 +95,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointersecond); } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) { QByteArray usernameSignature; - QByteArray domainUsernameSignature; + QString domainTokens; if (message->getBytesLeftToRead() > 0) { // read username from packet @@ -110,14 +110,14 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer> domainUsername; if (message->getBytesLeftToRead() > 0) { - // Read domain signature from packet. - packetStream >> domainUsernameSignature; + // Read domain tokens from packet. + packetStream >> domainTokens; } } } } - node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainUsernameSignature); + node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainTokens); } if (node) { @@ -452,7 +452,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QByteArray& domainUsernameSignature) { + const QString& domainTokens) { auto limitedNodeList = DependencyManager::get(); @@ -502,17 +502,18 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect QString verifiedDomainUsername; QStringList verifiedDomainUserGroups; if (domainHasLogin() && !domainUsername.isEmpty()) { - if (domainUsernameSignature.isEmpty()) { + if (domainTokens.isEmpty()) { // User is attempting to prove their domain identity. // ####### TODO: OAuth2 corollary of metaverse code, above. - getDomainGroupMemberships(domainUsernameSignature); // Optimistically get started on group memberships. + // ####### TODO: Do the following now? Probably can't! + //getDomainGroupMemberships(domainUsername); // Optimistically get started on group memberships. #ifdef WANT_DEBUG qDebug() << "stalling login because we have no domain username-signature:" << domainUsername; #endif return SharedNodePointer(); - } else if (verifyDomainUserSignature(domainUsername, domainUsernameSignature, nodeConnection.senderSockAddr)) { + } else if (verifyDomainUserSignature(domainUsername, domainTokens, nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. getDomainGroupMemberships(domainUsername); verifiedDomainUsername = domainUsername.toLower(); @@ -742,10 +743,10 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, } bool DomainGatekeeper::verifyDomainUserSignature(const QString& domainUsername, - const QByteArray& domainUsernameSignature, + const QString& domainTokens, const HifiSockAddr& senderSockAddr) { - // ####### TODO: Verify via domain OAuth2. + // ####### TODO: Verify response from domain OAuth2 request to WordPress, if it's arrived yet. bool success = true; if (success) { return true; diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 99bd875457..0cb757a9ea 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -79,13 +79,13 @@ private: const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QByteArray& domainUsernameSignature); + const QString& domainTokens); SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection); bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); - bool verifyDomainUserSignature(const QString& domainUsername, const QByteArray& domainUsernameSignature, + bool verifyDomainUserSignature(const QString& domainUsername, const QString& domainUsernameSignature, const HifiSockAddr& senderSockAddr); bool isWithinMaxCapacity(); diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index d36f4b5f09..cb0b93232e 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -11,6 +11,8 @@ #include "DomainAccountManager.h" +// ####### TODO: Check that all #includes are still needed. + #include #include @@ -43,7 +45,12 @@ Setting::Handle domainAccessTokenExpiresIn {"private/domainAccessTokenExpir Setting::Handle domainAccessTokenType {"private/domainAccessTokenType", "" }; -DomainAccountManager::DomainAccountManager() { +DomainAccountManager::DomainAccountManager() : + _authURL(), + _username(), + _access_token(), + _refresh_token() +{ connect(this, &DomainAccountManager::loginComplete, this, &DomainAccountManager::sendInterfaceAccessTokenToServer); } @@ -57,8 +64,11 @@ void DomainAccountManager::setAuthURL(const QUrl& authURL) { } } -void DomainAccountManager::requestAccessToken(const QString& login, const QString& password) { +void DomainAccountManager::requestAccessToken(const QString& username, const QString& password) { + _username = username; + _access_token = ""; + _refresh_token = ""; QNetworkRequest request; @@ -69,7 +79,7 @@ void DomainAccountManager::requestAccessToken(const QString& login, const QStrin QByteArray formData; formData.append("grant_type=password&"); - formData.append("username=" + QUrl::toPercentEncoding(login) + "&"); + formData.append("username=" + QUrl::toPercentEncoding(username) + "&"); formData.append("password=" + QUrl::toPercentEncoding(password) + "&"); formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); // ####### TODO: Include state? @@ -122,7 +132,7 @@ void DomainAccountManager::requestAccessTokenFinished() { } void DomainAccountManager::sendInterfaceAccessTokenToServer() { - // TODO: Send successful packet to the domain-server. + emit newTokens(); } bool DomainAccountManager::accessTokenIsExpired() { @@ -159,7 +169,11 @@ bool DomainAccountManager::hasValidAccessToken() { } void DomainAccountManager::setAccessTokenFromJSON(const QJsonObject& jsonObject) { + _access_token = jsonObject["access_token"].toString(); + _refresh_token = jsonObject["refresh_token"].toString(); + // ####### TODO: Enable and use these. + // ####### TODO: Protect these per AccountManager? /* domainAccessToken.set(jsonObject["access_token"].toString()); domainAccessRefreshToken.set(jsonObject["refresh_token"].toString()); diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index eb0f2659dd..08b625a246 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -25,10 +25,14 @@ public: void setAuthURL(const QUrl& authURL); + 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); + void requestAccessToken(const QString& username, const QString& password); void requestAccessTokenFinished(); signals: @@ -36,6 +40,7 @@ signals: void loginComplete(const QUrl& authURL); void loginFailed(); void logoutComplete(); + void newTokens(); private slots: @@ -46,6 +51,9 @@ private: void sendInterfaceAccessTokenToServer(); QUrl _authURL; + QString _username; // ####### TODO: Store elsewhere? + QString _access_token; // ####... "" + QString _refresh_token; // ####... "" }; #endif // hifi_DomainAccountManager_h diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 262ad0d2a4..16eba6c6a1 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,21 +486,20 @@ 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(); + 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 From 8cdd76a42e1b1d3562050531590db419b0f1eda9 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 2 Aug 2020 16:17:04 +1200 Subject: [PATCH 09/22] Verify user at domain server --- domain-server/src/DomainGatekeeper.cpp | 163 ++++++++++++++---- domain-server/src/DomainGatekeeper.h | 26 ++- .../networking/src/DomainAccountManager.cpp | 3 + libraries/networking/src/DomainHandler.h | 1 + libraries/networking/src/NodeList.cpp | 3 +- 5 files changed, 157 insertions(+), 39 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index a1581c26f1..210dabece1 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; - QString domainTokens; + + QString domainUsername; + QStringList domainTokens; if (message->getBytesLeftToRead() > 0) { // read username from packet @@ -111,13 +112,17 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointergetBytesLeftToRead() > 0) { // Read domain tokens from packet. - packetStream >> domainTokens; + + QString domainTokensString; + packetStream >> domainTokensString; + domainTokens = domainTokensString.split(":"); } } } } - node = processAgentConnectRequest(nodeConnection, username, usernameSignature, domainUsername, domainTokens); + node = processAgentConnectRequest(nodeConnection, username, usernameSignature, + domainUsername, domainTokens.value(0), domainTokens.value(1)); } if (node) { @@ -452,7 +457,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QString& domainTokens) { + const QString& domainAccessToken, + const QString& domainRefreshToken) { auto limitedNodeList = DependencyManager::get(); @@ -502,30 +508,39 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect QString verifiedDomainUsername; QStringList verifiedDomainUserGroups; if (domainHasLogin() && !domainUsername.isEmpty()) { - if (domainTokens.isEmpty()) { + + if (domainAccessToken.isEmpty()) { // User is attempting to prove their domain identity. - - // ####### TODO: OAuth2 corollary of metaverse code, above. - - // ####### TODO: Do the following now? Probably can't! - //getDomainGroupMemberships(domainUsername); // Optimistically get started on group memberships. #ifdef WANT_DEBUG - qDebug() << "stalling login because we have no domain username-signature:" << domainUsername; + qDebug() << "Stalling login because we have no domain OAuth2 tokens:" << domainUsername; #endif return SharedNodePointer(); - } else if (verifyDomainUserSignature(domainUsername, domainTokens, nodeConnection.senderSockAddr)) { + + } else if (!_verifiedDomainUserIdentities.contains(domainUsername) + || _verifiedDomainUserIdentities[domainUsername] != QPair(domainAccessToken, domainRefreshToken)) { + // ####### TODO: Write a function for the above test. + // User's domain identity needs to be confirmed. + if (_verifiedDomainUserIdentities.contains(domainUsername)) { + _verifiedDomainUserIdentities.remove(domainUsername); + } + requestDomainUser(domainUsername, domainAccessToken, domainRefreshToken); +#ifdef WANT_DEBUG + qDebug() << "Stalling login because we haven't authenticated user yet:" << domainUsername; +#endif + + } else if (verifyDomainUserSignature(domainUsername, domainAccessToken, domainRefreshToken, + nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. getDomainGroupMemberships(domainUsername); verifiedDomainUsername = domainUsername.toLower(); + } else { - // User's identity didn't check out. - - // ####### TODO: OAuth2 corollary of metaverse code, above. - + // User's domain identity didn't check out. #ifdef WANT_DEBUG - qDebug() << "stalling login because domain signature verification failed:" << domainUsername; + qDebug() << "Stalling login because domain user verification failed:" << domainUsername; #endif return SharedNodePointer(); + } } @@ -742,17 +757,17 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, return false; } -bool DomainGatekeeper::verifyDomainUserSignature(const QString& domainUsername, - const QString& domainTokens, - const HifiSockAddr& senderSockAddr) { +// ####### TODO: Rename to verifyDomainUser()? +bool DomainGatekeeper::verifyDomainUserSignature(const QString& username, const QString& accessToken, + const QString& refreshToken, const HifiSockAddr& senderSockAddr) { // ####### TODO: Verify response from domain OAuth2 request to WordPress, if it's arrived yet. - bool success = true; - if (success) { + // #### Or assume the verification step has already occurred? + if (_verifiedDomainUserIdentities.contains(username)) { return true; } - sendConnectionDeniedPacket("Error decrypting domain username signature.", senderSockAddr, + sendConnectionDeniedPacket("Error verifying domain user.", senderSockAddr, DomainHandler::ConnectionRefusedReason::LoginErrorDomain); return false; } @@ -1171,12 +1186,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: Base on server settings. - return true; -} - void DomainGatekeeper::initLocalIDManagement() { std::uniform_int_distribution sixteenBitRand; std::random_device randomDevice; @@ -1204,3 +1213,97 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) { _localIDs.insert(newLocalID); return newLocalID; } + + +bool DomainGatekeeper::domainHasLogin() { + // The domain may have its own users and groups. This is enabled in the server settings by ... ####### + // ####### TODO: Base on server settings. + return true; +} + +void DomainGatekeeper::requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken) { + + // ####### TODO: Move this further up the chain such that generates "invalid username or password" condition? + // Don't request identity for the standard psuedo-account-names. + if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) { + return; + } + + if (_inFlightDomainUserIdentityRequests.contains(username)) { + // Domain identify request for this username is already flight. + return; + } + _inFlightDomainUserIdentityRequests.insert(username, QPair(accessToken, refreshToken)); + + QString API_BASE = "http://127.0.0.1:9001/wp-json/"; + // Typically "http://oursite.com/wp-json/". + // However, if using non-pretty permalinks or otherwise get a 404 error then use "http://oursite.com/?rest_route=/". + + // ####### TODO: Confirm API w.r.t. OAuth2 plugin's capabilities. + // Get data pertaining to "me", the user who generated the access token. + QString API_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; + + // ####### TODO: Append a random key to check in response? + + QNetworkRequest request; + + request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + // ####### TODO: WordPress plugin's authorization requirements. + request.setRawHeader(QByteArray("Authorization"), QString("Bearer " + accessToken).toUtf8()); + + QByteArray formData; // No data to send. + + QUrl domainUserURL = API_BASE + API_ROUTE; + domainUserURL = "http://localhost:9002/resource"; // ####### TODO: Delete + request.setUrl(domainUserURL); + + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + + // ####### TODO: Handle invalid URL (e.g., set timeout or similar). + + 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()); + + auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (200 <= httpStatus && httpStatus < 300) { + // Success. + QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + // ####### Expected response: + /* + { + id: 2, + username : 'apiuser', + roles : ['subscriber'] , + } + */ + + // ####### TODO: Handle invalid / unexpected response. + + QString username = rootObject["username"].toString().toLower(); + // ####### TODO: Handle invalid username or one that isn't in the _inFlight list. + + if (_inFlightDomainUserIdentityRequests.contains(username)) { + // Success! Verified user. + _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); + _inFlightDomainUserIdentityRequests.remove(username); + } else { + // Unexpected response. + // ####### TODO + } + + } else { + // Failure. + // ####### TODO: Is this the best way to handle _inFlightDomainUserIdentityRequests? + // If there's a brief network glitch will it recover? + // Perhaps clear on a timer? Cancel timer upon subsequent successful responses? + _inFlightDomainUserIdentityRequests.clear(); + } +} diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 0cb757a9ea..eaf20a6285 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -72,6 +72,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,13 +83,14 @@ private: const QString& username, const QByteArray& usernameSignature, const QString& domainUsername, - const QString& domainTokens); + 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 QString& domainUsernameSignature, + bool verifyDomainUserSignature(const QString& username, const QString& accessToken, const QString& refreshToken, const HifiSockAddr& senderSockAddr); bool isWithinMaxCapacity(); @@ -135,20 +140,25 @@ private: // void getIsGroupMember(const QString& username, const QUuid groupID); void getDomainOwnerFriendsList(); - // Login and groups for domain, separate from metaverse. - bool domainHasLogin(); - void getDomainGroupMemberships(const QString& domainUserName); - QHash _domainGroupMemberships; // - // 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; // Keep track of domain user identity requests in progress. + DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users. + + void getDomainGroupMemberships(const QString& domainUserName); + QHash _domainGroupMemberships; // }; diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index cb0b93232e..7fc02893eb 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -101,9 +101,12 @@ void DomainAccountManager::requestAccessTokenFinished() { QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); const QJsonObject& rootObject = jsonResponse.object(); + // ####### TODO: Test HTTP response codes rather than object contains "error". + // #### reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200 if (!rootObject.contains("error")) { // ####### TODO: Process response scope? // ####### TODO: Process response state? + // ####### TODO: Check that token type == "Bearer"? if (!rootObject.contains("access_token") // ####### TODO: Does WordPRess plugin provide "expires_in"? 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 16eba6c6a1..539d044c1e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -499,7 +499,8 @@ void NodeList::sendDomainServerCheckIn() { auto domainAccountManager = DependencyManager::get(); if (!domainAccountManager->getUsername().isEmpty()) { packetStream << domainAccountManager->getUsername(); - packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken()); + if (!domainAccountManager->getAccessToken().isEmpty()) { + packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken()); } } From c3769a5f7426855d8acfb2e10a6af469b6ad9fc1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 2 Aug 2020 17:33:13 +1200 Subject: [PATCH 10/22] Get WordPress API path from domain server settings --- domain-server/resources/describe-settings.json | 8 +++++++- domain-server/src/DomainGatekeeper.cpp | 15 ++++++++------- libraries/networking/src/NodeList.cpp | 1 + 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 14c15e1e3d..ba019e3fe7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -87,7 +87,13 @@ { "name": "oauth2_url_base", "label": "Authentication URL Base", - "help": "The URL base that the Interface and domain-server will use to make API requests.", + "help": "The URL base 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 \"http://oursite.com/wp-json/\". However, if using non-pretty permalinks or otherwise get a 404 error then use \"http://oursite.com/?rest_route=/\".", "advanced": true } ] diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 210dabece1..29667b0fd5 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -450,6 +450,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo } const QString AUTHENTICATION_OAUTH2_URL_BASE = "authentication.oauth2_url_base"; +const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; @@ -1235,13 +1236,15 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } _inFlightDomainUserIdentityRequests.insert(username, QPair(accessToken, refreshToken)); - QString API_BASE = "http://127.0.0.1:9001/wp-json/"; - // Typically "http://oursite.com/wp-json/". - // However, if using non-pretty permalinks or otherwise get a 404 error then use "http://oursite.com/?rest_route=/". + QString apiBase = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString(); + if (!apiBase.endsWith("/")) { + apiBase += "/"; + } - // ####### TODO: Confirm API w.r.t. OAuth2 plugin's capabilities. // Get data pertaining to "me", the user who generated the access token. - QString API_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; + // ####### TODO: Confirm API_ROUTE w.r.t. OAuth2 plugin's capabilities. + QString API_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; + QUrl domainUserURL = apiBase + API_ROUTE; // ####### TODO: Append a random key to check in response? @@ -1254,8 +1257,6 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& QByteArray formData; // No data to send. - QUrl domainUserURL = API_BASE + API_ROUTE; - domainUserURL = "http://localhost:9002/resource"; // ####### TODO: Delete request.setUrl(domainUserURL); request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index 539d044c1e..e02a8dd56e 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -501,6 +501,7 @@ void NodeList::sendDomainServerCheckIn() { packetStream << domainAccountManager->getUsername(); if (!domainAccountManager->getAccessToken().isEmpty()) { packetStream << (domainAccountManager->getAccessToken() + ":" + domainAccountManager->getRefreshToken()); + } } } From fdb4a5605a58a5e4c0d76325e4467e64195da192 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 2 Aug 2020 20:54:09 +1200 Subject: [PATCH 11/22] Tidy processing user connect request --- domain-server/src/DomainGatekeeper.cpp | 31 ++++++++++++++------------ domain-server/src/DomainGatekeeper.h | 5 +++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 29667b0fd5..ee8da95d6c 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -517,20 +517,15 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect #endif return SharedNodePointer(); - } else if (!_verifiedDomainUserIdentities.contains(domainUsername) - || _verifiedDomainUserIdentities[domainUsername] != QPair(domainAccessToken, domainRefreshToken)) { - // ####### TODO: Write a function for the above test. + } else if (needToVerifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken)) { // User's domain identity needs to be confirmed. - if (_verifiedDomainUserIdentities.contains(domainUsername)) { - _verifiedDomainUserIdentities.remove(domainUsername); - } requestDomainUser(domainUsername, domainAccessToken, domainRefreshToken); #ifdef WANT_DEBUG qDebug() << "Stalling login because we haven't authenticated user yet:" << domainUsername; #endif - } else if (verifyDomainUserSignature(domainUsername, domainAccessToken, domainRefreshToken, - nodeConnection.senderSockAddr)) { + } else if (verifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken, + nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. getDomainGroupMemberships(domainUsername); verifiedDomainUsername = domainUsername.toLower(); @@ -758,13 +753,17 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, return false; } -// ####### TODO: Rename to verifyDomainUser()? -bool DomainGatekeeper::verifyDomainUserSignature(const QString& username, const QString& accessToken, - const QString& refreshToken, const HifiSockAddr& senderSockAddr) { - // ####### TODO: Verify response from domain OAuth2 request to WordPress, if it's arrived yet. - // #### Or assume the verification step has already occurred? - if (_verifiedDomainUserIdentities.contains(username)) { +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; } @@ -1236,6 +1235,10 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } _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 += "/"; diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index eaf20a6285..263d5b853d 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -90,8 +90,9 @@ private: bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr); - bool verifyDomainUserSignature(const QString& username, const QString& accessToken, const QString& refreshToken, - 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(); From dfbad4efc097fd4a9c930034822ea3ac707d5c11 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 2 Aug 2020 21:22:59 +1200 Subject: [PATCH 12/22] Use "enable domain authentication" server setting --- domain-server/resources/describe-settings.json | 2 +- domain-server/src/DomainGatekeeper.cpp | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index ba019e3fe7..2593740969 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -93,7 +93,7 @@ { "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 \"http://oursite.com/wp-json/\". However, if using non-pretty permalinks or otherwise get a 404 error then use \"http://oursite.com/?rest_route=/\".", + "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 } ] diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index ee8da95d6c..6227ddb634 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -449,6 +449,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo return newNode; } +const QString AUTHENTICATION_ENAABLED = "authentication.enable_oauth2"; const QString AUTHENTICATION_OAUTH2_URL_BASE = "authentication.oauth2_url_base"; const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; @@ -1216,9 +1217,11 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) { bool DomainGatekeeper::domainHasLogin() { - // The domain may have its own users and groups. This is enabled in the server settings by ... ####### - // ####### TODO: Base on server settings. - return true; + // The domain may have its own users and groups in a WordPress site. + // ####### TODO: Add checks of any further domain server settings used. + return _server->_settingsManager.valueForKeyPath(AUTHENTICATION_ENAABLED).toBool() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_BASE).toString().isEmpty() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString().isEmpty(); } void DomainGatekeeper::requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken) { From 43b6e77235afe395ac3684419a86064a3710504c Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2020 09:17:12 +1200 Subject: [PATCH 13/22] Tidy networking code --- .../resources/describe-settings.json | 6 +-- domain-server/src/DomainGatekeeper.cpp | 37 ++++++------- domain-server/src/DomainGatekeeper.h | 2 +- .../networking/src/DomainAccountManager.cpp | 54 ++++++++++--------- .../networking/src/DomainAccountManager.h | 4 +- 5 files changed, 54 insertions(+), 49 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 2593740969..5560fdba56 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -85,9 +85,9 @@ "backup": false }, { - "name": "oauth2_url_base", - "label": "Authentication URL Base", - "help": "The URL base that the Interface will use to login via OAuth2.", + "name": "oauth2_url_path", + "label": "Authentication URL", + "help": "The URL that the Interface will use to login via OAuth2.", "advanced": true }, { diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 6227ddb634..16c7383bf9 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -450,7 +450,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo } const QString AUTHENTICATION_ENAABLED = "authentication.enable_oauth2"; -const QString AUTHENTICATION_OAUTH2_URL_BASE = "authentication.oauth2_url_base"; +const QString AUTHENTICATION_OAUTH2_URL_PATH = "authentication.oauth2_url_path"; const QString AUTHENTICATION_WORDPRESS_URL_BASE = "authentication.wordpress_url_base"; const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity"; const QString MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION = "security.maximum_user_capacity_redirect_location"; @@ -548,10 +548,9 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { if (domainHasLogin()) { QString domainAuthURL; - auto domainAuthURLVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_BASE); + auto domainAuthURLVariant = _server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH); if (domainAuthURLVariant.canConvert()) { domainAuthURL = domainAuthURLVariant.toString(); - qDebug() << "Domain authorization URL:" << domainAuthURL; } sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, domainAuthURL); @@ -1220,7 +1219,7 @@ 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. return _server->_settingsManager.valueForKeyPath(AUTHENTICATION_ENAABLED).toBool() - && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_BASE).toString().isEmpty() + && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_OAUTH2_URL_PATH).toString().isEmpty() && !_server->_settingsManager.valueForKeyPath(AUTHENTICATION_WORDPRESS_URL_BASE).toString().isEmpty(); } @@ -1233,7 +1232,7 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } if (_inFlightDomainUserIdentityRequests.contains(username)) { - // Domain identify request for this username is already flight. + // Domain identify request for this username is already in progress. return; } _inFlightDomainUserIdentityRequests.insert(username, QPair(accessToken, refreshToken)); @@ -1248,9 +1247,9 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } // Get data pertaining to "me", the user who generated the access token. - // ####### TODO: Confirm API_ROUTE w.r.t. OAuth2 plugin's capabilities. - QString API_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; - QUrl domainUserURL = apiBase + API_ROUTE; + // ####### TODO: Confirm API route and data w.r.t. OAuth2 plugin's capabilities. [plugin] + const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; + QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE; // ####### TODO: Append a random key to check in response? @@ -1258,7 +1257,6 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - // ####### TODO: WordPress plugin's authorization requirements. request.setRawHeader(QByteArray("Authorization"), QString("Bearer " + accessToken).toUtf8()); QByteArray formData; // No data to send. @@ -1278,12 +1276,13 @@ 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) { // Success. - QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); - const QJsonObject& rootObject = jsonResponse.object(); - // ####### Expected response: + // ####### TODO: Verify Expected response. [plugin] /* { id: 2, @@ -1292,22 +1291,24 @@ void DomainGatekeeper::requestDomainUserFinished() { } */ - // ####### TODO: Handle invalid / unexpected response. - QString username = rootObject["username"].toString().toLower(); - // ####### TODO: Handle invalid username or one that isn't in the _inFlight list. - if (_inFlightDomainUserIdentityRequests.contains(username)) { // Success! Verified user. _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); _inFlightDomainUserIdentityRequests.remove(username); } else { - // Unexpected response. - // ####### TODO + // Failure. + qDebug() << "Unexpected username in response for user details -" << username; } + // ####### TODO: Handle roles if available. [plugin] + } else { // Failure. + + // ####### TODO: Error fields to report. [plugin] + qDebug() << "Error in response for user details -" << rootObject["error"].toString(); + // ####### TODO: Is this the best way to handle _inFlightDomainUserIdentityRequests? // If there's a brief network glitch will it recover? // Perhaps clear on a timer? Cancel timer upon subsequent successful responses? diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 263d5b853d..0e1da5d6ee 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -155,7 +155,7 @@ private: void requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken); typedef QHash> DomainUserIdentities; // > - DomainUserIdentities _inFlightDomainUserIdentityRequests; // Keep track of domain user identity requests in progress. + DomainUserIdentities _inFlightDomainUserIdentityRequests; // Domain user identity requests currently in progress. DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users. void getDomainGroupMemberships(const QString& domainUserName); diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 7fc02893eb..5cf707e732 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -60,7 +60,12 @@ void DomainAccountManager::setAuthURL(const QUrl& authURL) { qCDebug(networking) << "AccountManager URL for authenticated requests has been changed to" << qPrintable(_authURL.toString()); - // ####### TODO: See AccountManager::setAuthURL(). + _access_token = ""; + _refresh_token = ""; + + // ####### TODO: Restore and refresh OAuth2 tokens if have them for this domain. + + // ####### TODO: Handle "keep me logged in". } } @@ -84,9 +89,7 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); // ####### TODO: Include state? - QUrl domainAuthURL = _authURL; - domainAuthURL.setPath("/token"); // ####### TODO: miniOrange-mandated URL. ####### TODO: Should this be included in the server settings value? - request.setUrl(domainAuthURL); + request.setUrl(_authURL); request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); @@ -96,40 +99,40 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt } void DomainAccountManager::requestAccessTokenFinished() { + QNetworkReply* requestReply = reinterpret_cast(sender()); QJsonDocument jsonResponse = QJsonDocument::fromJson(requestReply->readAll()); const QJsonObject& rootObject = jsonResponse.object(); - // ####### TODO: Test HTTP response codes rather than object contains "error". - // #### reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200 - if (!rootObject.contains("error")) { - // ####### TODO: Process response scope? - // ####### TODO: Process response state? + auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (200 <= httpStatus && httpStatus < 300) { + // ####### TODO: Check that token type == "Bearer"? + // ####### TODO: Process response state? + // ####### TODO: Process response scope? - if (!rootObject.contains("access_token") - // ####### TODO: Does WordPRess plugin provide "expires_in"? - // If so, handle here, or is it just the domain server that needs to use it? - //|| !rootObject.contains("expires_in") - || !rootObject.contains("token_type")) { + if (rootObject.contains("access_token")) { + // Success. - qCDebug(networking) << "Received a response for password grant that is missing one or more expected values."; - emit loginFailed(); - - } else { - - // clear the path from the response URL so we have the right root URL for this access token QUrl rootURL = requestReply->url(); rootURL.setPath(""); - qCDebug(networking) << "Storing a domain account with access-token for" << qPrintable(rootURL.toString()); - setAccessTokenFromJSON(rootObject); + setTokensFromJSON(rootObject, rootURL); - emit loginComplete(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. + + // ####### TODO: Error object fields to report. [plugin] + qCDebug(networking) << "Error in response for password grant -" << rootObject["error"].toString(); emit loginFailed(); } } @@ -171,13 +174,14 @@ 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? /* + 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)); diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 08b625a246..6578963bf8 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -37,7 +37,7 @@ public slots: void requestAccessTokenFinished(); signals: void authRequired(); - void loginComplete(const QUrl& authURL); + void loginComplete(); void loginFailed(); void logoutComplete(); void newTokens(); @@ -47,7 +47,7 @@ private slots: private: bool hasValidAccessToken(); bool accessTokenIsExpired(); - void setAccessTokenFromJSON(const QJsonObject&); + void setTokensFromJSON(const QJsonObject&, const QUrl& url); void sendInterfaceAccessTokenToServer(); QUrl _authURL; From 26e6ce07d7068973d192300e9e8b5f74407fcc44 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2020 11:26:11 +1200 Subject: [PATCH 14/22] Further networking tidying --- domain-server/src/DomainGatekeeper.cpp | 17 +++++------ .../networking/src/DomainAccountManager.cpp | 29 +++++++------------ 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 16c7383bf9..25f38d6ebd 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -1101,11 +1101,11 @@ void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply void DomainGatekeeper::getDomainGroupMemberships(const QString& domainUserName) { - // ####### TODO: Get user's domain group memberships (WordPress roles) from domain. + // ####### TODO: Get user's domain group memberships (WordPress roles) from domain. [plugin groups] // This may be able to be provided at the same time as the "authenticate user" call to the domain API, in which case // a copy of some of the following code can be made there. However, this code is still needed for refreshing groups. - // ####### TODO: Check how often this method and the WordPress API is called. + // ####### TODO: Check how often this method and the WordPress API is called. [plugin groups] QStringList wordpressGroupsForUser; wordpressGroupsForUser << "silVER" << "gold" << "coal"; @@ -1161,7 +1161,7 @@ void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply* req qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply->error(); } -// ####### TODO: Domain equivalent or addition +// ####### 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(); @@ -1251,7 +1251,7 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE; - // ####### TODO: Append a random key to check in response? + // ####### TODO: Append a random key to check in response? [security] QNetworkRequest request; @@ -1265,8 +1265,6 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - // ####### TODO: Handle invalid URL (e.g., set timeout or similar). - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkReply* requestReply = networkAccessManager.post(request, formData); connect(requestReply, &QNetworkReply::finished, this, &DomainGatekeeper::requestDomainUserFinished); @@ -1280,6 +1278,7 @@ void DomainGatekeeper::requestDomainUserFinished() { const QJsonObject& rootObject = jsonResponse.object(); auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (200 <= httpStatus && httpStatus < 300) { // Success. // ####### TODO: Verify Expected response. [plugin] @@ -1307,11 +1306,9 @@ void DomainGatekeeper::requestDomainUserFinished() { // Failure. // ####### TODO: Error fields to report. [plugin] - qDebug() << "Error in response for user details -" << rootObject["error"].toString(); + qDebug() << "Error in response for user details -" << httpStatus << requestReply->error() + << "-" << rootObject["error"].toString(); - // ####### TODO: Is this the best way to handle _inFlightDomainUserIdentityRequests? - // If there's a brief network glitch will it recover? - // Perhaps clear on a timer? Cancel timer upon subsequent successful responses? _inFlightDomainUserIdentityRequests.clear(); } } diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 5cf707e732..cb67d30d3e 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -11,31 +11,23 @@ #include "DomainAccountManager.h" -// ####### TODO: Check that all #includes are still needed. - -#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" // FIXME: Generalize to other OAuth2 sources for domain login. const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; -const QString DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE = "foo bar"; // ####### TODO: WordPress plugin's required scope. -// ####### TODO: Should scope be configured in domain server settings? +const QString DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE = "foo bar"; // ####### TODO: WordPress plugin's required scope. [plugin] +// ####### TODO: Should scope be configured in domain server settings? [plugin] // ####### TODO: Add storing domain URL and check against it when retrieving values? // ####### TODO: Add storing _authURL and check against it when retrieving values? @@ -79,7 +71,7 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - // ####### TODO: WordPress plugin's authorization requirements. + // ####### TODO: WordPress plugin's authorization requirements. [plugin] request.setRawHeader(QByteArray("Authorization"), QByteArray("Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x")); QByteArray formData; @@ -87,7 +79,7 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt formData.append("username=" + QUrl::toPercentEncoding(username) + "&"); formData.append("password=" + QUrl::toPercentEncoding(password) + "&"); formData.append("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); - // ####### TODO: Include state? + // ####### TODO: Include state? [plugin] request.setUrl(_authURL); @@ -108,10 +100,10 @@ void DomainAccountManager::requestAccessTokenFinished() { auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (200 <= httpStatus && httpStatus < 300) { - // ####### TODO: Check that token type == "Bearer"? - // ####### TODO: Process response state? - // ####### TODO: Process response scope? + // ####### TODO: Process response state? [plugin] + // ####### TODO: Process response scope? [plugin] + // ####### TODO: Which method are the tokens provided in? [plugin] if (rootObject.contains("access_token")) { // Success. @@ -132,7 +124,8 @@ void DomainAccountManager::requestAccessTokenFinished() { // Failure. // ####### TODO: Error object fields to report. [plugin] - qCDebug(networking) << "Error in response for password grant -" << rootObject["error"].toString(); + qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error() + << "_" << rootObject["error"].toString(); emit loginFailed(); } } From 0b42ef57489579f05cbcef4a6c2aed6c9ac6f7fc Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2020 13:21:20 +1200 Subject: [PATCH 15/22] Regularize domain username case --- domain-server/src/DomainGatekeeper.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 25f38d6ebd..4b0eaab662 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -109,6 +109,7 @@ 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 tokens from packet. @@ -528,8 +529,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect } else if (verifyDomainUserIdentity(domainUsername, domainAccessToken, domainRefreshToken, nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. - getDomainGroupMemberships(domainUsername); - verifiedDomainUsername = domainUsername.toLower(); + verifiedDomainUsername = domainUsername; + getDomainGroupMemberships(verifiedDomainUsername); } else { // User's domain identity didn't check out. From 257eadc99f84b97fe6a6cc4cfc1db0b0011cdec1 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Mon, 3 Aug 2020 16:12:13 +1200 Subject: [PATCH 16/22] "Unable connect to this domain" message box after login if can't connect --- domain-server/src/DomainGatekeeper.cpp | 4 ++-- interface/src/ConnectionMonitor.cpp | 6 ++++++ libraries/networking/src/AccountManager.cpp | 4 +++- libraries/networking/src/DomainHandler.cpp | 4 +++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 4b0eaab662..8d0bc17f00 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -553,10 +553,10 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (domainAuthURLVariant.canConvert()) { domainAuthURL = domainAuthURLVariant.toString(); } - sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", + sendConnectionDeniedPacket("You lack the required domain permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::NotAuthorizedDomain, domainAuthURL); } 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 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/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/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 0a61036b96..7049f2aef6 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -550,7 +550,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer Date: Tue, 4 Aug 2020 09:15:05 +1200 Subject: [PATCH 17/22] Misc. tidying --- domain-server/src/DomainGatekeeper.cpp | 8 +------- domain-server/src/DomainServerSettingsManager.cpp | 2 +- interface/src/Application.cpp | 1 - 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 8d0bc17f00..9019a530d7 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -1218,7 +1218,7 @@ Node::LocalID DomainGatekeeper::findOrCreateLocalID(const QUuid& uuid) { 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. + // ####### 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(); @@ -1226,12 +1226,6 @@ bool DomainGatekeeper::domainHasLogin() { void DomainGatekeeper::requestDomainUser(const QString& username, const QString& accessToken, const QString& refreshToken) { - // ####### TODO: Move this further up the chain such that generates "invalid username or password" condition? - // Don't request identity for the standard psuedo-account-names. - if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) { - return; - } - if (_inFlightDomainUserIdentityRequests.contains(username)) { // Domain identify request for this username is already in progress. return; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 30ca15a51e..82ac0b265a 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -2186,7 +2186,7 @@ QList DomainServerSettingsManager::getBlacklistGroupIDs() { } QStringList DomainServerSettingsManager::getDomainGroupNames() { - // Names as configured in domain server; not necessarily mnetaverse groups. + // Names as configured in domain server; not necessarily metaverse groups. QSet result; foreach(NodePermissionsKey groupKey, _groupPermissions.keys()) { result += _groupPermissions[groupKey]->getID(); 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); } From bc56eb5ac75b5f677c01faa72e5f39f325f4f477 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 17:12:31 +1200 Subject: [PATCH 18/22] Update to work with WordPress plugin --- .../resources/describe-settings.json | 14 ++++---- domain-server/src/DomainGatekeeper.cpp | 36 +++++++++---------- .../networking/src/DomainAccountManager.cpp | 26 +++++--------- .../networking/src/DomainAccountManager.h | 2 ++ libraries/networking/src/DomainHandler.cpp | 4 ++- 5 files changed, 36 insertions(+), 46 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 5560fdba56..c901772296 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -77,13 +77,6 @@ "type": "checkbox", "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.", - "advanced": true, - "backup": false - }, { "name": "oauth2_url_path", "label": "Authentication URL", @@ -95,6 +88,13 @@ "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 } ] }, diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 9019a530d7..ad637f3c17 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -453,6 +453,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo 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"; @@ -553,8 +554,15 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect 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); + 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); @@ -1242,11 +1250,9 @@ void DomainGatekeeper::requestDomainUser(const QString& username, const QString& } // Get data pertaining to "me", the user who generated the access token. - // ####### TODO: Confirm API route and data w.r.t. OAuth2 plugin's capabilities. [plugin] - const QString WORDPRESS_USER_ROUTE = "wp/v2/users/me?context=edit&_fields=id,username,roles"; - QUrl domainUserURL = apiBase + WORDPRESS_USER_ROUTE; - - // ####### TODO: Append a random key to check in response? [security] + 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; @@ -1275,34 +1281,24 @@ void DomainGatekeeper::requestDomainUserFinished() { auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (200 <= httpStatus && httpStatus < 300) { - // Success. - // ####### TODO: Verify Expected response. [plugin] - /* - { - id: 2, - username : 'apiuser', - roles : ['subscriber'] , - } - */ QString username = rootObject["username"].toString().toLower(); if (_inFlightDomainUserIdentityRequests.contains(username)) { // Success! Verified user. _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); _inFlightDomainUserIdentityRequests.remove(username); + + // ####### TODO: Handle roles. + } else { // Failure. qDebug() << "Unexpected username in response for user details -" << username; } - // ####### TODO: Handle roles if available. [plugin] - } else { // Failure. - - // ####### TODO: Error fields to report. [plugin] qDebug() << "Error in response for user details -" << httpStatus << requestReply->error() - << "-" << rootObject["error"].toString(); + << "-" << rootObject["error"].toString() << rootObject["error_description"].toString(); _inFlightDomainUserIdentityRequests.clear(); } diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index cb67d30d3e..9910252317 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -26,8 +26,6 @@ // FIXME: Generalize to other OAuth2 sources for domain login. const bool VERBOSE_HTTP_REQUEST_DEBUGGING = false; -const QString DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE = "foo bar"; // ####### TODO: WordPress plugin's required scope. [plugin] -// ####### TODO: Should scope be configured in domain server settings? [plugin] // ####### TODO: Add storing domain URL and check against it when retrieving values? // ####### TODO: Add storing _authURL and check against it when retrieving values? @@ -71,15 +69,15 @@ void DomainAccountManager::requestAccessToken(const QString& username, const QSt request.setHeader(QNetworkRequest::UserAgentHeader, NetworkingConstants::VIRCADIA_USER_AGENT); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - // ####### TODO: WordPress plugin's authorization requirements. [plugin] - request.setRawHeader(QByteArray("Authorization"), QByteArray("Basic b2F1dGgtY2xpZW50LTE6b2F1dGgtY2xpZW50LXNlY3JldC0x")); + // 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("scope=" + DOMAIN_ACCOUNT_MANAGER_REQUESTED_SCOPE); - // ####### TODO: Include state? [plugin] + formData.append("client_id=" + _clientID); request.setUrl(_authURL); @@ -100,20 +98,13 @@ void DomainAccountManager::requestAccessTokenFinished() { auto httpStatus = requestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (200 <= httpStatus && httpStatus < 300) { - // ####### TODO: Process response state? [plugin] - // ####### TODO: Process response scope? [plugin] - - // ####### TODO: Which method are the tokens provided in? [plugin] + // miniOrange plugin provides no scope. if (rootObject.contains("access_token")) { // Success. - 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."; @@ -122,10 +113,8 @@ void DomainAccountManager::requestAccessTokenFinished() { } else { // Failure. - - // ####### TODO: Error object fields to report. [plugin] - qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error() - << "_" << rootObject["error"].toString(); + qCDebug(networking) << "Error in response for password grant -" << httpStatus << requestReply->error() + << "-" << rootObject["error"].toString() << rootObject["error_description"].toString(); emit loginFailed(); } } @@ -173,6 +162,7 @@ void DomainAccountManager::setTokensFromJSON(const QJsonObject& jsonObject, cons // ####### 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()); diff --git a/libraries/networking/src/DomainAccountManager.h b/libraries/networking/src/DomainAccountManager.h index 6578963bf8..213d40a8d8 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -24,6 +24,7 @@ public: DomainAccountManager(); void setAuthURL(const QUrl& authURL); + void setClientID(const QString& clientID) { _clientID = clientID; } QString getUsername() { return _username; } QString getAccessToken() { return _access_token; } @@ -51,6 +52,7 @@ private: void sendInterfaceAccessTokenToServer(); QUrl _authURL; + QString _clientID; QString _username; // ####### TODO: Store elsewhere? QString _access_token; // ####... "" QString _refresh_token; // ####... "" diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 7049f2aef6..c1a24748f4 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -589,7 +589,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer(); if (!extraInfo.isEmpty()) { - accountManager->setAuthURL(extraInfo); + auto extraInfoComponents = extraInfo.split("|"); + accountManager->setAuthURL(extraInfoComponents.value(0)); + accountManager->setClientID(extraInfoComponents.value(1)); } if (!_hasCheckedForDomainAccessToken) { From 3a43283e7b1d03545c85b7bf671f1fe9bc64a635 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 19:42:02 +1200 Subject: [PATCH 19/22] Provide domain name to domain login dialog --- .../resources/qml/LoginDialog/LinkAccountBody.qml | 1 + interface/src/ui/DialogsManager.cpp | 14 ++++++++++---- interface/src/ui/DialogsManager.h | 5 ++++- interface/src/ui/LoginDialog.cpp | 4 ++++ interface/src/ui/LoginDialog.h | 1 + libraries/networking/src/DomainAccountManager.cpp | 5 ++++- libraries/networking/src/DomainAccountManager.h | 3 ++- 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/LoginDialog/LinkAccountBody.qml b/interface/resources/qml/LoginDialog/LinkAccountBody.qml index 3d715d1a39..810a8a37dd 100644 --- a/interface/resources/qml/LoginDialog/LinkAccountBody.qml +++ b/interface/resources/qml/LoginDialog/LinkAccountBody.qml @@ -46,6 +46,7 @@ Item { readonly property bool loginDialogPoppedUp: loginDialog.getLoginDialogPoppedUp() readonly property bool isLoggingInToDomain: loginDialog.getDomainLoginRequested() + readonly property string domainLoginDomain: loginDialog.getDomainLoginDomain() QtObject { id: d diff --git a/interface/src/ui/DialogsManager.cpp b/interface/src/ui/DialogsManager.cpp index 848663967f..6e807c7b9f 100644 --- a/interface/src/ui/DialogsManager.cpp +++ b/interface/src/ui/DialogsManager.cpp @@ -110,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(); } @@ -120,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(); } @@ -129,8 +135,8 @@ void DialogsManager::hideLoginDialog() { } -void DialogsManager::showDomainLoginDialog() { - _isDomainLogin = true; +void DialogsManager::showDomainLoginDialog(const QString& domain) { + setDomainLogin(true, domain); LoginDialog::showWithSelection(); } diff --git a/interface/src/ui/DialogsManager.h b/interface/src/ui/DialogsManager.h index 30127ced68..fa5c589fb4 100644 --- a/interface/src/ui/DialogsManager.h +++ b/interface/src/ui/DialogsManager.h @@ -42,6 +42,7 @@ public: void emitAddressBarShown(bool visible) { emit addressBarShown(visible); } void setAddressBarVisible(bool addressBarVisible); bool getIsDomainLogin() { return _isDomainLogin; } + QString getDomainLoginDomain() { return _domainLoginDomain; } public slots: void showAddressBar(); @@ -51,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); @@ -86,7 +87,9 @@ private: 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 3cc37bcadb..c7597c5658 100644 --- a/interface/src/ui/LoginDialog.cpp +++ b/interface/src/ui/LoginDialog.cpp @@ -427,3 +427,7 @@ void LoginDialog::signupFailed(QNetworkReply* reply) { 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 310a5db255..9f4af5debb 100644 --- a/interface/src/ui/LoginDialog.h +++ b/interface/src/ui/LoginDialog.h @@ -85,6 +85,7 @@ protected slots: Q_INVOKABLE bool getLoginDialogPoppedUp() const; Q_INVOKABLE bool getDomainLoginRequested() const; + Q_INVOKABLE QString getDomainLoginDomain() const; }; diff --git a/libraries/networking/src/DomainAccountManager.cpp b/libraries/networking/src/DomainAccountManager.cpp index 9910252317..6bb868df61 100644 --- a/libraries/networking/src/DomainAccountManager.cpp +++ b/libraries/networking/src/DomainAccountManager.cpp @@ -184,7 +184,10 @@ bool DomainAccountManager::checkAndSignalForAccessToken() { // 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 213d40a8d8..0388ba1f5a 100644 --- a/libraries/networking/src/DomainAccountManager.h +++ b/libraries/networking/src/DomainAccountManager.h @@ -36,8 +36,9 @@ public slots: void requestAccessToken(const QString& username, const QString& password); void requestAccessTokenFinished(); + signals: - void authRequired(); + void authRequired(const QString& domain); void loginComplete(); void loginFailed(); void logoutComplete(); From 60048162c086911dce0f09e267d63b61e85f44ca Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 21:00:09 +1200 Subject: [PATCH 20/22] Use WordPress user roles as domain groups --- domain-server/src/DomainGatekeeper.cpp | 25 ++++++++----------------- domain-server/src/DomainGatekeeper.h | 1 - 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index ad637f3c17..90a6ee9f5f 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -531,7 +531,6 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect nodeConnection.senderSockAddr)) { // User's domain identity is confirmed. verifiedDomainUsername = domainUsername; - getDomainGroupMemberships(verifiedDomainUsername); } else { // User's domain identity didn't check out. @@ -1108,20 +1107,6 @@ void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply* requestReply } -void DomainGatekeeper::getDomainGroupMemberships(const QString& domainUserName) { - - // ####### TODO: Get user's domain group memberships (WordPress roles) from domain. [plugin groups] - // This may be able to be provided at the same time as the "authenticate user" call to the domain API, in which case - // a copy of some of the following code can be made there. However, this code is still needed for refreshing groups. - - // ####### TODO: Check how often this method and the WordPress API is called. [plugin groups] - - QStringList wordpressGroupsForUser; - wordpressGroupsForUser << "silVER" << "gold" << "coal"; - _domainGroupMemberships[domainUserName] = wordpressGroupsForUser; -} - - void DomainGatekeeper::getDomainOwnerFriendsList() { JSONCallbackParameters callbackParams; callbackParams.callbackReceiver = this; @@ -1282,13 +1267,19 @@ void DomainGatekeeper::requestDomainUserFinished() { if (200 <= httpStatus && httpStatus < 300) { - QString username = rootObject["username"].toString().toLower(); + QString username = rootObject.value("username").toString().toLower(); if (_inFlightDomainUserIdentityRequests.contains(username)) { // Success! Verified user. _verifiedDomainUserIdentities.insert(username, _inFlightDomainUserIdentityRequests.value(username)); _inFlightDomainUserIdentityRequests.remove(username); - // ####### TODO: Handle roles. + // User user's WordPress roles as domain groups. + QStringList domainUserGroups; + auto userRoles = rootObject.value("roles").toArray(); + foreach (auto role, userRoles) { + domainUserGroups.append(role.toString()); + } + _domainGroupMemberships[username] = domainUserGroups; } else { // Failure. diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 0e1da5d6ee..cf41786e4a 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -158,7 +158,6 @@ private: DomainUserIdentities _inFlightDomainUserIdentityRequests; // Domain user identity requests currently in progress. DomainUserIdentities _verifiedDomainUserIdentities; // Verified domain users. - void getDomainGroupMemberships(const QString& domainUserName); QHash _domainGroupMemberships; // }; From 6f9b47c07d307a34403fa611098eb6be8e48e106 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 21:31:13 +1200 Subject: [PATCH 21/22] Distinguish domain groups with a leading "@" --- domain-server/src/DomainGatekeeper.cpp | 7 ++++--- domain-server/src/DomainGatekeeper.h | 2 ++ domain-server/src/DomainServerSettingsManager.cpp | 4 ++++ domain-server/src/DomainServerSettingsManager.h | 8 ++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 90a6ee9f5f..5b0c526d9f 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -181,7 +181,7 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin 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". + // For example, "@silver @Gold, @platinum". auto domainGroups = _server->_settingsManager.getDomainGroupNames() .filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$", QRegularExpression::CaseInsensitiveOption)); @@ -302,7 +302,7 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin 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". + // For example, "@silver @Gold, @platinum". auto domainGroups = _server->_settingsManager.getDomainBlacklistGroupNames() .filter(QRegularExpression("^(.*[\\s,])?" + userGroup + "([\\s,].*)?$", QRegularExpression::CaseInsensitiveOption)); @@ -1277,7 +1277,8 @@ void DomainGatekeeper::requestDomainUserFinished() { QStringList domainUserGroups; auto userRoles = rootObject.value("roles").toArray(); foreach (auto role, userRoles) { - domainUserGroups.append(role.toString()); + // Distinguish domain groups from metaverse groups by a leading special character. + domainUserGroups.append(DOMAIN_GROUP_CHAR + role.toString().toLower()); } _domainGroupMemberships[username] = domainUserGroups; diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index cf41786e4a..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 { diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 82ac0b265a..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). diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 8c18c22b32..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"; From dfef1fb5bbcb1c5fcc31d3f520f6c16ced524e08 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 4 Aug 2020 22:03:08 +1200 Subject: [PATCH 22/22] Remove unused domain server setting --- domain-server/resources/describe-settings.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index c901772296..8e57a9b683 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -69,14 +69,6 @@ "type": "checkbox", "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", - "advanced": true - }, { "name": "oauth2_url_path", "label": "Authentication URL",