|  | // Copyright 2021 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | #include "chrome/browser/policy/cbcm_invalidations_initializer.h" | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/json/json_writer.h" | 
|  | #include "base/values.h" | 
|  | #include "chrome/browser/device_identity/device_oauth2_token_service.h" | 
|  | #include "chrome/browser/device_identity/device_oauth2_token_service_factory.h" | 
|  | #include "chrome/browser/device_identity/device_oauth2_token_store_desktop.h" | 
|  | #include "components/os_crypt/os_crypt_mocker.h" | 
|  | #include "components/policy/core/common/cloud/mock_cloud_policy_client.h" | 
|  | #include "components/prefs/testing_pref_service.h" | 
|  | #include "content/public/test/browser_task_environment.h" | 
|  | #include "google_apis/gaia/gaia_urls.h" | 
|  | #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" | 
|  | #include "services/network/test/test_url_loader_factory.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | namespace policy { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | static const char kFirstRefreshToken[] = "first_refresh_token"; | 
|  | static const char kSecondRefreshToken[] = "second_refresh_token"; | 
|  | static const char kFirstAccessToken[] = "first_access_token"; | 
|  | static const char kSecondAccessToken[] = "second_access_token"; | 
|  | static const char kServiceAccountEmail[] = | 
|  | "service_account@system.gserviceaccount.com"; | 
|  | static const char kOtherServiceAccountEmail[] = | 
|  | "other_service_account@system.gserviceaccount.com"; | 
|  | static const char kDMToken[] = "dm_token"; | 
|  | static const char kAuthCode[] = "auth_code"; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class FakeCloudPolicyClient : public MockCloudPolicyClient { | 
|  | public: | 
|  | void FetchRobotAuthCodes( | 
|  | DMAuth auth, | 
|  | enterprise_management::DeviceServiceApiAccessRequest::DeviceType | 
|  | device_type, | 
|  | const std::set<std::string>& oauth_scopes, | 
|  | RobotAuthCodeCallback callback) override { | 
|  | std::move(callback).Run(DM_STATUS_SUCCESS, kAuthCode); | 
|  | } | 
|  | }; | 
|  |  | 
|  | class CBCMInvalidationsInitializerTest | 
|  | : public testing::Test, | 
|  | public CBCMInvalidationsInitializer::Delegate { | 
|  | public: | 
|  | CBCMInvalidationsInitializerTest() = default; | 
|  |  | 
|  | void RefreshTokenSavedCallbackExpectSuccess(bool success) { | 
|  | EXPECT_TRUE(success); | 
|  |  | 
|  | ++num_refresh_tokens_saved_; | 
|  | } | 
|  |  | 
|  | // CBCMInvalidationsInitializer::Delegate: | 
|  | void StartInvalidations() override { ++num_invalidations_started_; } | 
|  |  | 
|  | scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory() | 
|  | override { | 
|  | return test_url_loader_factory_.GetSafeWeakWrapper(); | 
|  | } | 
|  |  | 
|  | bool IsInvalidationsServiceStarted() const override { | 
|  | return num_invalidations_started_ > 0; | 
|  | } | 
|  |  | 
|  | protected: | 
|  | int num_refresh_tokens_saved() const { return num_refresh_tokens_saved_; } | 
|  |  | 
|  | int num_invalidations_started() const { return num_invalidations_started_; } | 
|  |  | 
|  | FakeCloudPolicyClient* policy_client() { return &mock_policy_client_; } | 
|  |  | 
|  | TestingPrefServiceSimple* testing_local_state() { | 
|  | return &testing_local_state_; | 
|  | } | 
|  |  | 
|  | network::TestURLLoaderFactory* test_url_loader_factory() { | 
|  | return &test_url_loader_factory_; | 
|  | } | 
|  |  | 
|  | std::string MakeTokensFromAuthCodesResponse(const std::string& refresh_token, | 
|  | const std::string& access_token) { | 
|  | base::Value::Dict dict; | 
|  | dict.Set("access_token", access_token); | 
|  | dict.Set("refresh_token", refresh_token); | 
|  | dict.Set("expires_in", 9999); | 
|  |  | 
|  | std::string json; | 
|  | base::JSONWriter::Write(dict, &json); | 
|  | return json; | 
|  | } | 
|  |  | 
|  | private: | 
|  | void SetUp() override { | 
|  | DeviceOAuth2TokenStoreDesktop::RegisterPrefs( | 
|  | testing_local_state_.registry()); | 
|  | DeviceOAuth2TokenServiceFactory::Initialize(GetURLLoaderFactory(), | 
|  | &testing_local_state_); | 
|  | OSCryptMocker::SetUp(); | 
|  | mock_policy_client_.SetDMToken(kDMToken); | 
|  | } | 
|  |  | 
|  | void TearDown() override { | 
|  | DeviceOAuth2TokenServiceFactory::Shutdown(); | 
|  | OSCryptMocker::TearDown(); | 
|  | } | 
|  |  | 
|  | int num_refresh_tokens_saved_ = 0; | 
|  | int num_invalidations_started_ = 0; | 
|  |  | 
|  | FakeCloudPolicyClient mock_policy_client_; | 
|  | network::TestURLLoaderFactory test_url_loader_factory_; | 
|  | content::BrowserTaskEnvironment task_environment_; | 
|  | TestingPrefServiceSimple testing_local_state_; | 
|  | }; | 
|  |  | 
|  | TEST_F(CBCMInvalidationsInitializerTest, InvalidationsStartDisabled) { | 
|  | CBCMInvalidationsInitializer initializer(this); | 
|  |  | 
|  | EXPECT_FALSE(IsInvalidationsServiceStarted()); | 
|  | } | 
|  |  | 
|  | TEST_F(CBCMInvalidationsInitializerTest, | 
|  | InvalidationsStartIfRefreshTokenPresent) { | 
|  | CBCMInvalidationsInitializer initializer(this); | 
|  |  | 
|  | DeviceOAuth2TokenServiceFactory::Get()->SetServiceAccountEmail( | 
|  | kServiceAccountEmail); | 
|  | DeviceOAuth2TokenServiceFactory::Get()->SetAndSaveRefreshToken( | 
|  | kFirstRefreshToken, | 
|  | base::BindRepeating(&CBCMInvalidationsInitializerTest:: | 
|  | RefreshTokenSavedCallbackExpectSuccess, | 
|  | base::Unretained(this))); | 
|  |  | 
|  | EXPECT_EQ(1, num_refresh_tokens_saved()); | 
|  | EXPECT_TRUE( | 
|  | DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable()); | 
|  |  | 
|  | EXPECT_FALSE(IsInvalidationsServiceStarted()); | 
|  |  | 
|  | initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail); | 
|  |  | 
|  | EXPECT_TRUE(IsInvalidationsServiceStarted()); | 
|  | } | 
|  |  | 
|  | TEST_F(CBCMInvalidationsInitializerTest, | 
|  | InvalidationsStartIfRefreshTokenAbsent) { | 
|  | CBCMInvalidationsInitializer initializer(this); | 
|  |  | 
|  | EXPECT_FALSE(IsInvalidationsServiceStarted()); | 
|  |  | 
|  | initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail); | 
|  | EXPECT_EQ(1, test_url_loader_factory()->NumPending()); | 
|  | EXPECT_EQ(GaiaUrls::GetInstance()->oauth2_token_url().spec(), | 
|  | test_url_loader_factory()->GetPendingRequest(0)->request.url); | 
|  |  | 
|  | EXPECT_TRUE(IsInvalidationsServiceStarted()); | 
|  |  | 
|  | EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest( | 
|  | GaiaUrls::GetInstance()->oauth2_token_url().spec(), | 
|  | MakeTokensFromAuthCodesResponse(kFirstRefreshToken, kFirstAccessToken))); | 
|  |  | 
|  | EXPECT_TRUE(IsInvalidationsServiceStarted()); | 
|  | } | 
|  |  | 
|  | TEST_F(CBCMInvalidationsInitializerTest, | 
|  | InvalidationsDontRestartOnNextPolicyFetch) { | 
|  | CBCMInvalidationsInitializer initializer(this); | 
|  |  | 
|  | EXPECT_FALSE(IsInvalidationsServiceStarted()); | 
|  |  | 
|  | initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail); | 
|  |  | 
|  | EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest( | 
|  | GaiaUrls::GetInstance()->oauth2_token_url().spec(), | 
|  | MakeTokensFromAuthCodesResponse(kFirstRefreshToken, kFirstAccessToken))); | 
|  |  | 
|  | EXPECT_TRUE(IsInvalidationsServiceStarted()); | 
|  | EXPECT_EQ(0, test_url_loader_factory()->NumPending()); | 
|  |  | 
|  | // When the next policy fetch happens, it'll contain the same service account. | 
|  | // In this case, avoid starting the invalidations services again. | 
|  | initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail); | 
|  | EXPECT_EQ(0, test_url_loader_factory()->NumPending()); | 
|  |  | 
|  | EXPECT_EQ(1, num_invalidations_started()); | 
|  | } | 
|  |  | 
|  | TEST_F(CBCMInvalidationsInitializerTest, | 
|  | CanHandleServiceAccountChangedAfterFetchingInSameSession) { | 
|  | CBCMInvalidationsInitializer initializer(this); | 
|  |  | 
|  | EXPECT_FALSE(IsInvalidationsServiceStarted()); | 
|  |  | 
|  | // Simulate that a policy sets a service account and triggers a fetch. | 
|  | initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail); | 
|  | EXPECT_TRUE(IsInvalidationsServiceStarted()); | 
|  | EXPECT_EQ(1, test_url_loader_factory()->NumPending()); | 
|  | EXPECT_EQ(GaiaUrls::GetInstance()->oauth2_token_url().spec(), | 
|  | test_url_loader_factory()->GetPendingRequest(0)->request.url); | 
|  | EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest( | 
|  | GaiaUrls::GetInstance()->oauth2_token_url().spec(), | 
|  | MakeTokensFromAuthCodesResponse(kFirstRefreshToken, kFirstAccessToken))); | 
|  |  | 
|  | EXPECT_EQ(0, test_url_loader_factory()->NumPending()); | 
|  | EXPECT_TRUE( | 
|  | DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable()); | 
|  | EXPECT_EQ(CoreAccountId::FromRobotEmail(kServiceAccountEmail), | 
|  | DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId()); | 
|  | std::string first_refresh_token = | 
|  | testing_local_state()->GetString(kCBCMServiceAccountRefreshToken); | 
|  |  | 
|  | // Simulate that a policy comes in with a different service account. This | 
|  | // should trigger a re-initialization of the service account. | 
|  | initializer.OnServiceAccountSet(policy_client(), kOtherServiceAccountEmail); | 
|  | EXPECT_EQ(1, test_url_loader_factory()->NumPending()); | 
|  | EXPECT_TRUE(IsInvalidationsServiceStarted()); | 
|  | EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest( | 
|  | GaiaUrls::GetInstance()->oauth2_token_url().spec(), | 
|  | MakeTokensFromAuthCodesResponse(kSecondRefreshToken, | 
|  | kSecondAccessToken))); | 
|  |  | 
|  | EXPECT_EQ(1, num_invalidations_started()); | 
|  | EXPECT_TRUE( | 
|  | DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable()); | 
|  | // Now a different refresh token and email should be present. The token | 
|  | // themselves aren't validated because they're encrypted. Verifying that it | 
|  | // changed is sufficient. | 
|  | EXPECT_EQ(CoreAccountId::FromRobotEmail(kOtherServiceAccountEmail), | 
|  | DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId()); | 
|  | EXPECT_NE(first_refresh_token, | 
|  | testing_local_state()->GetString(kCBCMServiceAccountRefreshToken)); | 
|  | } | 
|  |  | 
|  | TEST_F(CBCMInvalidationsInitializerTest, | 
|  | CanHandleServiceAccountChangedWhenAccountPresentOnStartup) { | 
|  | CBCMInvalidationsInitializer initializer(this); | 
|  |  | 
|  | // Set up the token service as if there was already a service account set up | 
|  | // on start up. | 
|  | DeviceOAuth2TokenServiceFactory::Get()->SetServiceAccountEmail( | 
|  | kServiceAccountEmail); | 
|  | DeviceOAuth2TokenServiceFactory::Get()->SetAndSaveRefreshToken( | 
|  | kFirstRefreshToken, | 
|  | base::BindRepeating(&CBCMInvalidationsInitializerTest:: | 
|  | RefreshTokenSavedCallbackExpectSuccess, | 
|  | base::Unretained(this))); | 
|  |  | 
|  | EXPECT_EQ(1, num_refresh_tokens_saved()); | 
|  | EXPECT_TRUE( | 
|  | DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable()); | 
|  | std::string first_refresh_token = | 
|  | testing_local_state()->GetString(kCBCMServiceAccountRefreshToken); | 
|  |  | 
|  | EXPECT_FALSE(IsInvalidationsServiceStarted()); | 
|  | // On first policy store load, this will be called and invalidations started. | 
|  | initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail); | 
|  | EXPECT_EQ(0, test_url_loader_factory()->NumPending()); | 
|  | EXPECT_TRUE(IsInvalidationsServiceStarted()); | 
|  | // The same refresh token should be present in local state. | 
|  | EXPECT_EQ(first_refresh_token, | 
|  | testing_local_state()->GetString(kCBCMServiceAccountRefreshToken)); | 
|  |  | 
|  | // Simulate that a new policy is fetched with a different service account. | 
|  | // This should result in a gaia call for the service account initialization. | 
|  | initializer.OnServiceAccountSet(policy_client(), kOtherServiceAccountEmail); | 
|  | EXPECT_EQ(1, test_url_loader_factory()->NumPending()); | 
|  | EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest( | 
|  | GaiaUrls::GetInstance()->oauth2_token_url().spec(), | 
|  | MakeTokensFromAuthCodesResponse(kSecondRefreshToken, | 
|  | kSecondAccessToken))); | 
|  |  | 
|  | EXPECT_TRUE(IsInvalidationsServiceStarted()); | 
|  | EXPECT_TRUE( | 
|  | DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable()); | 
|  | // Now a different refresh token and email should be present. The token | 
|  | // themselves aren't validated because they're encrypted. Verifying that it | 
|  | // changed is sufficient. | 
|  | EXPECT_EQ(CoreAccountId::FromRobotEmail(kOtherServiceAccountEmail), | 
|  | DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId()); | 
|  | EXPECT_NE(first_refresh_token, | 
|  | testing_local_state()->GetString(kCBCMServiceAccountRefreshToken)); | 
|  | } | 
|  |  | 
|  | }  // namespace policy |