blob: 1bd2bb78c7140ffe8d82e5053dd8b07763390737 [file] [log] [blame]
// 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/functional/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/sync/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