| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/policy/cloud/component_cloud_policy_service.h" |
| |
| #include "base/callback.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/pickle.h" |
| #include "base/run_loop.h" |
| #include "base/sha1.h" |
| #include "base/single_thread_task_runner.h" |
| #include "base/stl_util.h" |
| #include "base/values.h" |
| #include "chrome/browser/policy/cloud/cloud_policy_constants.h" |
| #include "chrome/browser/policy/cloud/mock_cloud_policy_client.h" |
| #include "chrome/browser/policy/cloud/mock_cloud_policy_store.h" |
| #include "chrome/browser/policy/cloud/policy_builder.h" |
| #include "chrome/browser/policy/cloud/resource_cache.h" |
| #include "chrome/browser/policy/external_data_fetcher.h" |
| #include "chrome/browser/policy/policy_domain_descriptor.h" |
| #include "chrome/browser/policy/policy_map.h" |
| #include "chrome/browser/policy/policy_types.h" |
| #include "chrome/browser/policy/proto/cloud/chrome_extension_policy.pb.h" |
| #include "chrome/browser/policy/proto/cloud/device_management_backend.pb.h" |
| #include "components/policy/core/common/schema.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/test/test_browser_thread.h" |
| #include "net/url_request/test_url_fetcher_factory.h" |
| #include "net/url_request/url_fetcher_delegate.h" |
| #include "net/url_request/url_request_context.h" |
| #include "net/url_request/url_request_context_getter.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace em = enterprise_management; |
| |
| using testing::Mock; |
| |
| namespace policy { |
| |
| namespace { |
| |
| const char kTestExtension[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; |
| const char kTestExtension2[] = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; |
| const char kTestDownload[] = "http://example.com/getpolicy?id=123"; |
| const char kTestDownload2[] = "http://example.com/getpolicy?id=456"; |
| |
| const char kTestPolicy[] = |
| "{" |
| " \"Name\": {" |
| " \"Value\": \"disabled\"" |
| " }," |
| " \"Second\": {" |
| " \"Value\": \"maybe\"," |
| " \"Level\": \"Recommended\"" |
| " }" |
| "}"; |
| |
| const char kTestSchema[] = |
| "{" |
| " \"type\": \"object\"," |
| " \"properties\": {" |
| " \"Name\": { \"type\": \"string\" }," |
| " \"Second\": { \"type\": \"string\" }" |
| " }" |
| "}"; |
| |
| class MockComponentCloudPolicyDelegate |
| : public ComponentCloudPolicyService::Delegate { |
| public: |
| virtual ~MockComponentCloudPolicyDelegate() {} |
| |
| MOCK_METHOD0(OnComponentCloudPolicyRefreshNeeded, void()); |
| MOCK_METHOD0(OnComponentCloudPolicyUpdated, void()); |
| }; |
| |
| class TestURLRequestContextGetter : public net::URLRequestContextGetter { |
| public: |
| explicit TestURLRequestContextGetter( |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner) |
| : task_runner_(task_runner) {} |
| virtual net::URLRequestContext* GetURLRequestContext() OVERRIDE { |
| return NULL; |
| } |
| virtual scoped_refptr<base::SingleThreadTaskRunner> |
| GetNetworkTaskRunner() const OVERRIDE { |
| return task_runner_; |
| } |
| |
| private: |
| virtual ~TestURLRequestContextGetter() {} |
| |
| scoped_refptr<base::SingleThreadTaskRunner> task_runner_; |
| }; |
| |
| } // namespace |
| |
| class ComponentCloudPolicyServiceTest : public testing::Test { |
| protected: |
| ComponentCloudPolicyServiceTest() |
| : ui_thread_(content::BrowserThread::UI, &loop_), |
| file_thread_(content::BrowserThread::FILE, &loop_) {} |
| |
| virtual void SetUp() OVERRIDE { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| cache_ = new ResourceCache(temp_dir_.path(), loop_.message_loop_proxy()); |
| service_.reset(new ComponentCloudPolicyService( |
| &delegate_, |
| &store_, |
| make_scoped_ptr(cache_), |
| loop_.message_loop_proxy(), |
| loop_.message_loop_proxy())); |
| |
| builder_.policy_data().set_policy_type( |
| dm_protocol::kChromeExtensionPolicyType); |
| builder_.policy_data().set_settings_entity_id(kTestExtension); |
| builder_.payload().set_download_url(kTestDownload); |
| builder_.payload().set_secure_hash(base::SHA1HashString(kTestPolicy)); |
| |
| expected_policy_.Set("Name", POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER, |
| base::Value::CreateStringValue("disabled"), NULL); |
| expected_policy_.Set("Second", POLICY_LEVEL_RECOMMENDED, POLICY_SCOPE_USER, |
| base::Value::CreateStringValue("maybe"), NULL); |
| |
| // A NULL |request_context_| is enough to construct the updater, but |
| // ComponentCloudPolicyService::Backend::LoadStore() tests the pointer when |
| // Connect() is called before the store was loaded. |
| request_context_ = |
| new TestURLRequestContextGetter(loop_.message_loop_proxy()); |
| } |
| |
| virtual void TearDown() OVERRIDE { |
| // The service cleans up its backend on the FILE thread. |
| service_.reset(); |
| RunUntilIdle(); |
| } |
| |
| void RunUntilIdle() { |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void LoadStore() { |
| EXPECT_FALSE(store_.is_initialized()); |
| EXPECT_FALSE(service_->is_initialized()); |
| |
| em::PolicyData* data = new em::PolicyData(); |
| data->set_username(ComponentPolicyBuilder::kFakeUsername); |
| data->set_request_token(ComponentPolicyBuilder::kFakeToken); |
| store_.policy_.reset(data); |
| |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyUpdated()); |
| store_.NotifyStoreLoaded(); |
| RunUntilIdle(); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| EXPECT_TRUE(service_->is_initialized()); |
| } |
| |
| void PopulateCache() { |
| Pickle pickle; |
| pickle.WriteString(kTestExtension); |
| pickle.WriteString(kTestExtension2); |
| std::string data(reinterpret_cast<const char*>(pickle.data()), |
| pickle.size()); |
| EXPECT_TRUE( |
| cache_->Store(ComponentCloudPolicyService::kComponentNamespaceCache, |
| dm_protocol::kChromeExtensionPolicyType, |
| data)); |
| |
| EXPECT_TRUE(cache_->Store( |
| "extension-policy", kTestExtension, CreateSerializedResponse())); |
| EXPECT_TRUE( |
| cache_->Store("extension-policy-data", kTestExtension, kTestPolicy)); |
| |
| builder_.policy_data().set_settings_entity_id(kTestExtension2); |
| EXPECT_TRUE(cache_->Store( |
| "extension-policy", kTestExtension2, CreateSerializedResponse())); |
| EXPECT_TRUE( |
| cache_->Store("extension-policy-data", kTestExtension2, kTestPolicy)); |
| |
| EXPECT_TRUE(cache_->Store("unrelated", "stuff", "here")); |
| } |
| |
| scoped_ptr<em::PolicyFetchResponse> CreateResponse() { |
| builder_.Build(); |
| return make_scoped_ptr(new em::PolicyFetchResponse(builder_.policy())); |
| } |
| |
| std::string CreateSerializedResponse() { |
| builder_.Build(); |
| return builder_.GetBlob(); |
| } |
| |
| scoped_ptr<SchemaOwner> CreateTestSchema() { |
| std::string error; |
| scoped_ptr<SchemaOwner> schema = SchemaOwner::Parse(kTestSchema, &error); |
| EXPECT_TRUE(schema) << error; |
| return schema.Pass(); |
| } |
| |
| base::MessageLoop loop_; |
| content::TestBrowserThread ui_thread_; |
| content::TestBrowserThread file_thread_; |
| base::ScopedTempDir temp_dir_; |
| scoped_refptr<TestURLRequestContextGetter> request_context_; |
| net::TestURLFetcherFactory fetcher_factory_; |
| MockComponentCloudPolicyDelegate delegate_; |
| // |cache_| is owned by the |service_| and is invalid once the |service_| |
| // is destroyed. |
| ResourceCache* cache_; |
| MockCloudPolicyClient client_; |
| MockCloudPolicyStore store_; |
| scoped_ptr<ComponentCloudPolicyService> service_; |
| ComponentPolicyBuilder builder_; |
| PolicyMap expected_policy_; |
| }; |
| |
| TEST_F(ComponentCloudPolicyServiceTest, InitializeWithoutCredentials) { |
| EXPECT_FALSE(service_->is_initialized()); |
| // Run the background task to initialize the backend. |
| RunUntilIdle(); |
| // Still waiting for the |store_|. |
| EXPECT_FALSE(service_->is_initialized()); |
| |
| // Initialize with a store without credentials. |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyUpdated()); |
| store_.NotifyStoreLoaded(); |
| RunUntilIdle(); |
| EXPECT_TRUE(service_->is_initialized()); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| const PolicyBundle empty_bundle; |
| EXPECT_TRUE(service_->policy().Equals(empty_bundle)); |
| } |
| |
| TEST_F(ComponentCloudPolicyServiceTest, InitializationWithCachedComponents) { |
| // Store a previous list of components. |
| Pickle pickle; |
| pickle.WriteString("aa"); |
| pickle.WriteString("bb"); |
| pickle.WriteString("cc"); |
| std::string data(reinterpret_cast<const char*>(pickle.data()), pickle.size()); |
| EXPECT_TRUE( |
| cache_->Store(ComponentCloudPolicyService::kComponentNamespaceCache, |
| dm_protocol::kChromeExtensionPolicyType, |
| data)); |
| // Store some garbage in another domain; it won't be fetched. |
| EXPECT_TRUE(cache_->Store( |
| ComponentCloudPolicyService::kComponentNamespaceCache, "garbage", data)); |
| |
| // Connect a client before initialization is complete. |
| service_->Connect(&client_, request_context_); |
| EXPECT_TRUE(client_.namespaces_to_fetch_.empty()); |
| |
| // Now load the backend. |
| LoadStore(); |
| |
| // The cached namespaces were added to the client. |
| std::set<PolicyNamespaceKey> set; |
| set.insert(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, "aa")); |
| set.insert(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, "bb")); |
| set.insert(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, "cc")); |
| EXPECT_EQ(set, client_.namespaces_to_fetch_); |
| |
| // And the bad components were wiped from the cache. |
| std::map<std::string, std::string> contents; |
| cache_->LoadAllSubkeys(ComponentCloudPolicyService::kComponentNamespaceCache, |
| &contents); |
| ASSERT_EQ(1u, contents.size()); |
| EXPECT_EQ(std::string(dm_protocol::kChromeExtensionPolicyType), |
| contents.begin()->first); |
| } |
| |
| TEST_F(ComponentCloudPolicyServiceTest, ConnectAfterRegister) { |
| // Add some components. |
| scoped_refptr<PolicyDomainDescriptor> descriptor = new PolicyDomainDescriptor( |
| POLICY_DOMAIN_EXTENSIONS); |
| descriptor->RegisterComponent(kTestExtension, CreateTestSchema()); |
| descriptor->RegisterComponent(kTestExtension2, CreateTestSchema()); |
| service_->RegisterPolicyDomain(descriptor); |
| |
| // Now connect the client. |
| EXPECT_TRUE(client_.namespaces_to_fetch_.empty()); |
| service_->Connect(&client_, request_context_); |
| EXPECT_TRUE(client_.namespaces_to_fetch_.empty()); |
| |
| // It receives the namespaces once the backend is initialized. |
| LoadStore(); |
| std::set<PolicyNamespaceKey> set; |
| set.insert(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, |
| kTestExtension)); |
| set.insert(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, |
| kTestExtension2)); |
| EXPECT_EQ(set, client_.namespaces_to_fetch_); |
| |
| // The current components were also persisted to the cache. |
| std::map<std::string, std::string> contents; |
| cache_->LoadAllSubkeys(ComponentCloudPolicyService::kComponentNamespaceCache, |
| &contents); |
| ASSERT_EQ(1u, contents.size()); |
| EXPECT_EQ(std::string(dm_protocol::kChromeExtensionPolicyType), |
| contents.begin()->first); |
| const std::string data(contents.begin()->second); |
| Pickle pickle(data.data(), data.size()); |
| PickleIterator pickit(pickle); |
| std::string value0; |
| std::string value1; |
| std::string tmp; |
| ASSERT_TRUE(pickit.ReadString(&value0)); |
| ASSERT_TRUE(pickit.ReadString(&value1)); |
| EXPECT_FALSE(pickit.ReadString(&tmp)); |
| std::set<std::string> unpickled; |
| unpickled.insert(value0); |
| unpickled.insert(value1); |
| |
| std::set<std::string> expected_components; |
| expected_components.insert(kTestExtension); |
| expected_components.insert(kTestExtension2); |
| |
| EXPECT_EQ(expected_components, unpickled); |
| } |
| |
| TEST_F(ComponentCloudPolicyServiceTest, StoreReadyAfterConnectAndRegister) { |
| // Add some previous data to the cache. |
| PopulateCache(); |
| |
| // Add some components. |
| scoped_refptr<PolicyDomainDescriptor> descriptor = new PolicyDomainDescriptor( |
| POLICY_DOMAIN_EXTENSIONS); |
| descriptor->RegisterComponent(kTestExtension, CreateTestSchema()); |
| service_->RegisterPolicyDomain(descriptor); |
| |
| // And connect the client. Make the client have some policies, with a new |
| // download_url. |
| builder_.payload().set_download_url(kTestDownload2); |
| client_.SetPolicy(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, |
| kTestExtension), |
| *CreateResponse()); |
| service_->Connect(&client_, request_context_); |
| |
| // Now make the store ready. |
| LoadStore(); |
| |
| // The components that were registed before have their caches purged. |
| std::map<std::string, std::string> contents; |
| cache_->LoadAllSubkeys("extension-policy", &contents); |
| EXPECT_EQ(1u, contents.size()); // kTestExtension2 was purged. |
| EXPECT_TRUE(ContainsKey(contents, kTestExtension)); |
| cache_->LoadAllSubkeys("unrelated", &contents); |
| EXPECT_EQ(1u, contents.size()); // unrelated keys are not purged. |
| EXPECT_TRUE(ContainsKey(contents, "stuff")); |
| |
| // The policy responses that the client already had start updating. |
| net::TestURLFetcher* fetcher = fetcher_factory_.GetFetcherByID(0); |
| ASSERT_TRUE(fetcher); |
| // Expect the URL of the client's PolicyFetchResponse, not the cached URL. |
| EXPECT_EQ(GURL(kTestDownload2), fetcher->GetOriginalURL()); |
| } |
| |
| TEST_F(ComponentCloudPolicyServiceTest, ConnectThenRegisterThenStoreReady) { |
| // Connect right after creating the service. |
| service_->Connect(&client_, request_context_); |
| |
| // Now register the current components, before the backend has been |
| // initialized. |
| EXPECT_TRUE(client_.namespaces_to_fetch_.empty()); |
| scoped_refptr<PolicyDomainDescriptor> descriptor = new PolicyDomainDescriptor( |
| POLICY_DOMAIN_EXTENSIONS); |
| descriptor->RegisterComponent(kTestExtension, CreateTestSchema()); |
| service_->RegisterPolicyDomain(descriptor); |
| EXPECT_TRUE(client_.namespaces_to_fetch_.empty()); |
| |
| // Now load the store. The client gets the namespaces. |
| LoadStore(); |
| std::set<PolicyNamespaceKey> set; |
| set.insert(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, |
| kTestExtension)); |
| EXPECT_EQ(set, client_.namespaces_to_fetch_); |
| } |
| |
| TEST_F(ComponentCloudPolicyServiceTest, FetchPolicy) { |
| // Initialize the store and create the backend, and connect the client. |
| LoadStore(); |
| // A refresh is not needed, because no components were found. |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyRefreshNeeded()).Times(0); |
| service_->Connect(&client_, request_context_); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| |
| // Register the components to fetch. |
| scoped_refptr<PolicyDomainDescriptor> descriptor = new PolicyDomainDescriptor( |
| POLICY_DOMAIN_EXTENSIONS); |
| descriptor->RegisterComponent(kTestExtension, CreateTestSchema()); |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyRefreshNeeded()); |
| service_->RegisterPolicyDomain(descriptor); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| |
| // Send back a fake policy fetch response. |
| client_.SetPolicy(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, |
| kTestExtension), |
| *CreateResponse()); |
| service_->OnPolicyFetched(&client_); |
| RunUntilIdle(); |
| |
| // That should have triggered the download fetch. |
| net::TestURLFetcher* fetcher = fetcher_factory_.GetFetcherByID(0); |
| ASSERT_TRUE(fetcher); |
| EXPECT_EQ(GURL(kTestDownload), fetcher->GetOriginalURL()); |
| fetcher->set_response_code(200); |
| fetcher->SetResponseString(kTestPolicy); |
| fetcher->delegate()->OnURLFetchComplete(fetcher); |
| |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyUpdated()); |
| RunUntilIdle(); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| |
| // The policy is now being served. |
| PolicyNamespace ns(POLICY_DOMAIN_EXTENSIONS, kTestExtension); |
| PolicyBundle expected_bundle; |
| expected_bundle.Get(ns).CopyFrom(expected_policy_); |
| EXPECT_TRUE(service_->policy().Equals(expected_bundle)); |
| } |
| |
| TEST_F(ComponentCloudPolicyServiceTest, LoadAndPurgeCache) { |
| // Insert data in the cache. |
| PopulateCache(); |
| |
| // Load the initial cache. |
| LoadStore(); |
| |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyRefreshNeeded()); |
| service_->Connect(&client_, request_context_); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| |
| PolicyBundle expected_bundle; |
| PolicyNamespace ns(POLICY_DOMAIN_EXTENSIONS, kTestExtension); |
| expected_bundle.Get(ns).CopyFrom(expected_policy_); |
| ns.component_id = kTestExtension2; |
| expected_bundle.Get(ns).CopyFrom(expected_policy_); |
| EXPECT_TRUE(service_->policy().Equals(expected_bundle)); |
| |
| // Now purge one of the extensions. |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyUpdated()); |
| // The service will start updating the components that are registered, which |
| // starts by fetching policy for them. |
| scoped_refptr<PolicyDomainDescriptor> descriptor = new PolicyDomainDescriptor( |
| POLICY_DOMAIN_EXTENSIONS); |
| descriptor->RegisterComponent(kTestExtension2, CreateTestSchema()); |
| service_->RegisterPolicyDomain(descriptor); |
| RunUntilIdle(); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| |
| ns.component_id = kTestExtension; |
| expected_bundle.Get(ns).Clear(); |
| EXPECT_TRUE(service_->policy().Equals(expected_bundle)); |
| |
| std::map<std::string, std::string> contents; |
| cache_->LoadAllSubkeys("extension-policy", &contents); |
| EXPECT_EQ(1u, contents.size()); |
| EXPECT_TRUE(ContainsKey(contents, kTestExtension2)); |
| cache_->LoadAllSubkeys("unrelated", &contents); |
| EXPECT_EQ(1u, contents.size()); |
| EXPECT_TRUE(ContainsKey(contents, "stuff")); |
| } |
| |
| TEST_F(ComponentCloudPolicyServiceTest, UpdateCredentials) { |
| // Do the same as LoadStore() but without the initial credentials. |
| EXPECT_FALSE(store_.is_initialized()); |
| EXPECT_FALSE(service_->is_initialized()); |
| store_.policy_.reset(new em::PolicyData()); // No credentials. |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyUpdated()); |
| store_.NotifyStoreLoaded(); |
| RunUntilIdle(); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| EXPECT_TRUE(service_->is_initialized()); |
| |
| // Connect the client and register an extension. |
| service_->Connect(&client_, request_context_); |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyRefreshNeeded()); |
| scoped_refptr<PolicyDomainDescriptor> descriptor = new PolicyDomainDescriptor( |
| POLICY_DOMAIN_EXTENSIONS); |
| descriptor->RegisterComponent(kTestExtension, CreateTestSchema()); |
| service_->RegisterPolicyDomain(descriptor); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| |
| // Send the response to the service. The response data will be rejected, |
| // because the store doesn't have the updated credentials yet. |
| client_.SetPolicy(PolicyNamespaceKey(dm_protocol::kChromeExtensionPolicyType, |
| kTestExtension), |
| *CreateResponse()); |
| service_->OnPolicyFetched(&client_); |
| RunUntilIdle(); |
| |
| // The policy was not verified, and no download is started. |
| net::TestURLFetcher* fetcher = fetcher_factory_.GetFetcherByID(0); |
| EXPECT_FALSE(fetcher); |
| |
| // Now update the |store_| with the updated policy, which includes |
| // credentials. The responses in the |client_| will be reloaded. |
| em::PolicyData* data = new em::PolicyData(); |
| data->set_username(ComponentPolicyBuilder::kFakeUsername); |
| data->set_request_token(ComponentPolicyBuilder::kFakeToken); |
| store_.policy_.reset(data); |
| store_.NotifyStoreLoaded(); |
| RunUntilIdle(); |
| |
| // The extension policy was validated this time, and the download is started. |
| fetcher = fetcher_factory_.GetFetcherByID(0); |
| ASSERT_TRUE(fetcher); |
| EXPECT_EQ(GURL(kTestDownload), fetcher->GetOriginalURL()); |
| fetcher->set_response_code(200); |
| fetcher->SetResponseString(kTestPolicy); |
| fetcher->delegate()->OnURLFetchComplete(fetcher); |
| |
| EXPECT_CALL(delegate_, OnComponentCloudPolicyUpdated()); |
| RunUntilIdle(); |
| Mock::VerifyAndClearExpectations(&delegate_); |
| |
| // The policy is now being served. |
| PolicyNamespace ns(POLICY_DOMAIN_EXTENSIONS, kTestExtension); |
| PolicyBundle expected_bundle; |
| expected_bundle.Get(ns).CopyFrom(expected_policy_); |
| EXPECT_TRUE(service_->policy().Equals(expected_bundle)); |
| } |
| |
| } // namespace policy |