diff options
author | bartfab@chromium.org <bartfab@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-09 02:49:58 +0000 |
---|---|---|
committer | bartfab@chromium.org <bartfab@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-10-09 02:49:58 +0000 |
commit | 1a643611aac9777e310859b1b535796f7ade8424 (patch) | |
tree | a7d128ef5fe760d44bde3af7000e57adc6d413d3 | |
parent | 5906629ae1144b74c0a89700304f8a30d4461ef8 (diff) | |
download | chromium_src-1a643611aac9777e310859b1b535796f7ade8424.zip chromium_src-1a643611aac9777e310859b1b535796f7ade8424.tar.gz chromium_src-1a643611aac9777e310859b1b535796f7ade8424.tar.bz2 |
Allow explicitly whitelisted apps/extensions in public sessions
This CL adds an extension management policy provider that allows
explicitly whitelisted apps/extensions to be installed in public sessions.
Right now, QuickOffice and all hosted apps are whitelisted.
BUG=296868
TEST=New browser and unit tests
Committed: https://src.chromium.org/viewvc/chrome?view=rev&revision=226494
Review URL: https://codereview.chromium.org/24261010
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@227641 0039d316-1c4b-4281-b951-d872f2087c98
20 files changed, 721 insertions, 63 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index 7aea90f..ad4b6bb 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -4403,6 +4403,9 @@ Make sure you do not expose any sensitive information. <message name="IDS_EXTENSION_CANT_INSTALL_POLICY_BLOCKED" desc="Error message when a user tries to install an extension that is blocked by administrator policy."> <ph name="EXTENSION_NAME">$1<ex>Google Talk</ex></ph> (extension ID "<ph name="EXTENSION_ID">$2<ex>nckgahadagoaajjgafhacjanaoiihapd</ex></ph>") is blocked by the administrator. </message> + <message name="IDS_EXTENSION_CANT_INSTALL_IN_DEVICE_LOCAL_ACCOUNT" desc="Error message when a user tries to install or the administrator tries to force-install through policy an extension that is not allowed in a device-local account."> + <ph name="EXTENSION_NAME">$1<ex>Google Talk</ex></ph> (extension ID "<ph name="EXTENSION_ID">$2<ex>nckgahadagoaajjgafhacjanaoiihapd</ex></ph>") is not allowed in this type of session. + </message> <message name="IDS_EXTENSION_CANT_MODIFY_POLICY_REQUIRED" desc="Error message when a user tries to remove or change an extension that is required by administrator policy."> The administrator of this machine requires <ph name="EXTENSION_NAME">$1<ex>Google Talk</ex></ph> to be installed. It cannot be removed or modified. </message> diff --git a/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.cc b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.cc new file mode 100644 index 0000000..fd25e9c --- /dev/null +++ b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.cc @@ -0,0 +1,79 @@ +// Copyright 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/chromeos/extensions/device_local_account_management_policy_provider.h" + +#include <string> + +#include "base/logging.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/common/extensions/extension.h" +#include "extensions/common/manifest.h" +#include "grit/generated_resources.h" +#include "ui/base/l10n/l10n_util.h" + +namespace chromeos { + +namespace { + +// Apps/extensions explicitly whitelisted for use in device-local accounts. +const char* kDeviceLocalAccountWhitelist[] = { + "bpmcpldpdmajfigpchkicefoigmkfalc", // QuickOffice +}; + +} // namespace + +DeviceLocalAccountManagementPolicyProvider:: + DeviceLocalAccountManagementPolicyProvider( + policy::DeviceLocalAccount::Type account_type) + : account_type_(account_type) { +} + +DeviceLocalAccountManagementPolicyProvider:: + ~DeviceLocalAccountManagementPolicyProvider() { +} + +std::string DeviceLocalAccountManagementPolicyProvider:: + GetDebugPolicyProviderName() const { +#if defined(NDEBUG) + NOTREACHED(); + return std::string(); +#else + return "whitelist for device-local accounts"; +#endif +} + +bool DeviceLocalAccountManagementPolicyProvider::UserMayLoad( + const extensions::Extension* extension, + string16* error) const { + if (account_type_ == policy::DeviceLocalAccount::TYPE_KIOSK_APP) { + // For single-app kiosk sessions, allow only platform apps. + if (extension->GetType() == extensions::Manifest::TYPE_PLATFORM_APP) + return true; + + } else { + // Allow extension if its type is whitelisted for use in device-local + // accounts. + if (extension->GetType() == extensions::Manifest::TYPE_HOSTED_APP) + return true; + + // Allow extension if its specific ID is whitelisted for use in device-local + // accounts. + for (size_t i = 0; i < arraysize(kDeviceLocalAccountWhitelist); ++i) { + if (extension->id() == kDeviceLocalAccountWhitelist[i]) + return true; + } + } + + // Disallow all other extensions. + if (error) { + *error = l10n_util::GetStringFUTF16( + IDS_EXTENSION_CANT_INSTALL_IN_DEVICE_LOCAL_ACCOUNT, + UTF8ToUTF16(extension->name()), + UTF8ToUTF16(extension->id())); + } + return false; +} + +} // namespace chromeos diff --git a/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h new file mode 100644 index 0000000..c19fe1a --- /dev/null +++ b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h @@ -0,0 +1,38 @@ +// Copyright 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. + +#ifndef CHROME_BROWSER_CHROMEOS_EXTENSIONS_DEVICE_LOCAL_ACCOUNT_MANAGEMENT_POLICY_PROVIDER_H_ +#define CHROME_BROWSER_CHROMEOS_EXTENSIONS_DEVICE_LOCAL_ACCOUNT_MANAGEMENT_POLICY_PROVIDER_H_ + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "chrome/browser/chromeos/policy/device_local_account.h" +#include "chrome/browser/extensions/management_policy.h" + +namespace chromeos { + +// A managed policy for device-local accounts that ensures only extensions whose +// type or ID has been whitelisted for use in device-local accounts can be +// installed. +class DeviceLocalAccountManagementPolicyProvider + : public extensions::ManagementPolicy::Provider { + public: + explicit DeviceLocalAccountManagementPolicyProvider( + policy::DeviceLocalAccount::Type account_type); + virtual ~DeviceLocalAccountManagementPolicyProvider(); + + // extensions::ManagementPolicy::Provider: + virtual std::string GetDebugPolicyProviderName() const OVERRIDE; + virtual bool UserMayLoad(const extensions::Extension* extension, + string16* error) const OVERRIDE; + + private: + const policy::DeviceLocalAccount::Type account_type_; + + DISALLOW_COPY_AND_ASSIGN(DeviceLocalAccountManagementPolicyProvider); +}; + +} // namespace chromeos + +#endif // CHROME_BROWSER_CHROMEOS_EXTENSIONS_DEVICE_LOCAL_ACCOUNT_MANAGEMENT_POLICY_PROVIDER_H_ diff --git a/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc new file mode 100644 index 0000000..00802bf --- /dev/null +++ b/chrome/browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc @@ -0,0 +1,124 @@ +// Copyright 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/chromeos/extensions/device_local_account_management_policy_provider.h" + +#include <string> + +#include "base/files/file_path.h" +#include "base/memory/ref_counted.h" +#include "base/values.h" +#include "chrome/common/extensions/extension.h" +#include "extensions/common/manifest.h" +#include "extensions/common/manifest_constants.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace chromeos { + +namespace { + +const char kWhitelistedId[] = "bpmcpldpdmajfigpchkicefoigmkfalc"; + +scoped_refptr<const extensions::Extension> CreateExtensionFromValues( + const std::string& id, + base::DictionaryValue* values) { + values->SetString(extensions::manifest_keys::kName, "test"); + values->SetString(extensions::manifest_keys::kVersion, "0.1"); + std::string error; + return extensions::Extension::Create(base::FilePath(), + extensions::Manifest::INTERNAL, + *values, + extensions::Extension::NO_FLAGS, + id, + &error); +} + +scoped_refptr<const extensions::Extension> CreateExtension( + const std::string& id) { + base::DictionaryValue values; + return CreateExtensionFromValues(id, &values); +} + +scoped_refptr<const extensions::Extension> CreateHostedApp() { + base::DictionaryValue values; + values.Set(extensions::manifest_keys::kApp, new base::DictionaryValue); + values.Set(extensions::manifest_keys::kWebURLs, new base::ListValue); + return CreateExtensionFromValues(std::string(), &values); +} + +scoped_refptr<const extensions::Extension> CreatePlatformApp() { + base::DictionaryValue values; + values.Set(extensions::manifest_keys::kApp, new base::DictionaryValue); + values.Set(extensions::manifest_keys::kPlatformAppBackground, + new base::DictionaryValue); + values.Set(extensions::manifest_keys::kPlatformAppBackgroundPage, + new base::StringValue("background.html")); + return CreateExtensionFromValues(std::string(), &values); +} + +} // namespace + +TEST(DeviceLocalAccountManagementPolicyProviderTest, PublicSession) { + DeviceLocalAccountManagementPolicyProvider + provider(policy::DeviceLocalAccount::TYPE_PUBLIC_SESSION); + + // Verify that if an extension's type has been whitelisted for use in + // device-local accounts, the extension can be installed. + scoped_refptr<const extensions::Extension> extension = CreateHostedApp(); + ASSERT_TRUE(extension); + string16 error; + EXPECT_TRUE(provider.UserMayLoad(extension.get(), &error)); + EXPECT_EQ(string16(), error); + error.clear(); + + // Verify that if an extension's ID has been explicitly whitelisted for use in + // device-local accounts, the extension can be installed. + extension = CreateExtension(kWhitelistedId); + ASSERT_TRUE(extension); + EXPECT_TRUE(provider.UserMayLoad(extension.get(), &error)); + EXPECT_EQ(string16(), error); + error.clear(); + + // Verify that if neither the type nor the ID of an extension have been + // whitelisted for use in device-local accounts, the extension cannot be + // installed. + extension = CreateExtension(std::string()); + ASSERT_TRUE(extension); + EXPECT_FALSE(provider.UserMayLoad(extension.get(), &error)); + EXPECT_NE(string16(), error); + error.clear(); +} + +TEST(DeviceLocalAccountManagementPolicyProviderTest, KioskAppSession) { + DeviceLocalAccountManagementPolicyProvider + provider(policy::DeviceLocalAccount::TYPE_KIOSK_APP); + + // Verify that a platform app can be installed. + scoped_refptr<const extensions::Extension> extension = CreatePlatformApp(); + ASSERT_TRUE(extension); + string16 error; + EXPECT_TRUE(provider.UserMayLoad(extension.get(), &error)); + EXPECT_EQ(string16(), error); + error.clear(); + + // Verify that an extension whose type has been whitelisted for use in other + // types of device-local accounts cannot be installed in a single-app kiosk + // session. + extension = CreateHostedApp(); + ASSERT_TRUE(extension); + EXPECT_FALSE(provider.UserMayLoad(extension.get(), &error)); + EXPECT_NE(string16(), error); + error.clear(); + + // Verify that an extension whose ID has been whitelisted for use in other + // types of device-local accounts cannot be installed in a single-app kiosk + // session. + extension = CreateExtension(kWhitelistedId); + ASSERT_TRUE(extension); + EXPECT_FALSE(provider.UserMayLoad(extension.get(), &error)); + EXPECT_NE(string16(), error); + error.clear(); +} + +} // namespace chromeos diff --git a/chrome/browser/chromeos/login/user_manager_impl.cc b/chrome/browser/chromeos/login/user_manager_impl.cc index 68a4d38..c705639 100644 --- a/chrome/browser/chromeos/login/user_manager_impl.cc +++ b/chrome/browser/chromeos/login/user_manager_impl.cc @@ -362,11 +362,15 @@ void UserManagerImpl::UserLoggedIn(const std::string& email, return; } + policy::DeviceLocalAccount::Type device_local_account_type; if (email == UserManager::kGuestUserName) { GuestUserLoggedIn(); } else if (email == UserManager::kRetailModeUserName) { RetailModeUserLoggedIn(); - } else if (policy::IsKioskAppUser(email)) { + } else if (policy::IsDeviceLocalAccountUser(email, + &device_local_account_type) && + device_local_account_type == + policy::DeviceLocalAccount::TYPE_KIOSK_APP) { KioskAppLoggedIn(email); } else { EnsureUsersLoaded(); @@ -1402,7 +1406,11 @@ void UserManagerImpl::PublicAccountUserLoggedIn(User* user) { void UserManagerImpl::KioskAppLoggedIn(const std::string& username) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); - DCHECK(policy::IsKioskAppUser(username)); + policy::DeviceLocalAccount::Type device_local_account_type; + DCHECK(policy::IsDeviceLocalAccountUser(username, + &device_local_account_type)); + DCHECK_EQ(policy::DeviceLocalAccount::TYPE_KIOSK_APP, + device_local_account_type); active_user_ = User::CreateKioskAppUser(username); active_user_->SetStubImage(User::kInvalidImageIndex, false); diff --git a/chrome/browser/chromeos/policy/device_local_account.cc b/chrome/browser/chromeos/policy/device_local_account.cc index 2ad53ea..a614a9d 100644 --- a/chrome/browser/chromeos/policy/device_local_account.cc +++ b/chrome/browser/chromeos/policy/device_local_account.cc @@ -6,11 +6,13 @@ #include <set> +#include "base/basictypes.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/values.h" +#include "chrome/browser/chromeos/login/user_manager.h" #include "chrome/browser/chromeos/settings/cros_settings.h" #include "chrome/browser/chromeos/settings/cros_settings_names.h" #include "google_apis/gaia/gaia_auth_util.h" @@ -56,16 +58,35 @@ std::string GenerateDeviceLocalAccountUserId(const std::string& account_id, domain_prefix + kDeviceLocalAccountDomainSuffix); } -bool IsDeviceLocalAccountUser(const std::string& user_id) { - return EndsWith(gaia::ExtractDomainName(user_id), - kDeviceLocalAccountDomainSuffix, - true); -} +bool IsDeviceLocalAccountUser(const std::string& user_id, + DeviceLocalAccount::Type* type) { + // For historical reasons, the guest user ID does not contain an @ symbol and + // therefore, cannot be parsed by gaia::ExtractDomainName(). + if (user_id == chromeos::UserManager::kGuestUserName) + return false; + const std::string domain = gaia::ExtractDomainName(user_id); + if (!EndsWith(domain, kDeviceLocalAccountDomainSuffix, true)) + return false; + + const std::string domain_prefix = domain.substr( + 0, domain.size() - arraysize(kDeviceLocalAccountDomainSuffix) + 1); + + if (domain_prefix == kPublicAccountDomainPrefix) { + if (type) + *type = DeviceLocalAccount::TYPE_PUBLIC_SESSION; + return true; + } + if (domain_prefix == kKioskAppAccountDomainPrefix) { + if (type) + *type = DeviceLocalAccount::TYPE_KIOSK_APP; + return true; + } -bool IsKioskAppUser(const std::string& user_id) { - return gaia::ExtractDomainName(user_id) == - std::string(kKioskAppAccountDomainPrefix) + - kDeviceLocalAccountDomainSuffix; + // |user_id| is a device-local account but its type is not recognized. + NOTREACHED(); + if (type) + *type = DeviceLocalAccount::TYPE_COUNT; + return true; } void SetDeviceLocalAccounts( diff --git a/chrome/browser/chromeos/policy/device_local_account.h b/chrome/browser/chromeos/policy/device_local_account.h index 1e3e318..ff83461 100644 --- a/chrome/browser/chromeos/policy/device_local_account.h +++ b/chrome/browser/chromeos/policy/device_local_account.h @@ -40,9 +40,10 @@ struct DeviceLocalAccount { std::string GenerateDeviceLocalAccountUserId(const std::string& account_id, DeviceLocalAccount::Type type); -bool IsDeviceLocalAccountUser(const std::string& user_id); - -bool IsKioskAppUser(const std::string& user_id); +// Determines whether |user_id| belongs to a device-local account and if so, +// returns the type of device-local account in |type| unless |type| is NULL. +bool IsDeviceLocalAccountUser(const std::string& user_id, + DeviceLocalAccount::Type* type); // Stores a list of device-local accounts in |cros_settings|. The accounts are // stored as a list of dictionaries with each dictionary containing the diff --git a/chrome/browser/chromeos/policy/device_local_account_browsertest.cc b/chrome/browser/chromeos/policy/device_local_account_browsertest.cc index 59eb85b..aae8978 100644 --- a/chrome/browser/chromeos/policy/device_local_account_browsertest.cc +++ b/chrome/browser/chromeos/policy/device_local_account_browsertest.cc @@ -7,6 +7,7 @@ #include "base/basictypes.h" #include "base/bind.h" +#include "base/bind_helpers.h" #include "base/callback.h" #include "base/command_line.h" #include "base/file_util.h" @@ -17,6 +18,7 @@ #include "base/path_service.h" #include "base/run_loop.h" #include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "chrome/browser/browser_process.h" @@ -33,6 +35,8 @@ #include "chrome/browser/chromeos/policy/device_local_account.h" #include "chrome/browser/chromeos/policy/device_policy_builder.h" #include "chrome/browser/chromeos/policy/device_policy_cros_browser_test.h" +#include "chrome/browser/extensions/extension_service.h" +#include "chrome/browser/extensions/extension_system.h" #include "chrome/browser/lifetime/application_lifetime.h" #include "chrome/browser/policy/cloud/cloud_policy_constants.h" #include "chrome/browser/policy/cloud/policy_builder.h" @@ -52,11 +56,14 @@ #include "chrome/browser/ui/webui/chromeos/login/oobe_ui.h" #include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_switches.h" +#include "chrome/common/extensions/extension.h" #include "chromeos/chromeos_switches.h" #include "chromeos/dbus/cryptohome_client.h" #include "chromeos/dbus/dbus_method_call_status.h" #include "chromeos/dbus/fake_session_manager_client.h" #include "chromeos/dbus/session_manager_client.h" +#include "content/public/browser/notification_details.h" +#include "content/public/browser/notification_source.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_ui.h" #include "content/public/test/browser_test_utils.h" @@ -64,7 +71,11 @@ #include "crypto/rsa_private_key.h" #include "grit/chromium_strings.h" #include "grit/generated_resources.h" +#include "net/base/url_util.h" +#include "net/http/http_status_code.h" #include "net/test/embedded_test_server/embedded_test_server.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" #include "testing/gmock/include/gmock/gmock.h" #include "third_party/cros_system_api/dbus/service_constants.h" #include "ui/base/l10n/l10n_util.h" @@ -90,6 +101,129 @@ const char* kStartupURLs[] = { }; const char kExistentTermsOfServicePath[] = "chromeos/enterprise/tos.txt"; const char kNonexistentTermsOfServicePath[] = "chromeos/enterprise/tos404.txt"; +const char kRelativeUpdateURL[] = "/service/update2/crx"; +const char kUpdateManifestHeader[] = + "<?xml version='1.0' encoding='UTF-8'?>\n" + "<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>\n"; +const char kUpdateManifestTemplate[] = + " <app appid='%s'>\n" + " <updatecheck codebase='%s' version='%s' />\n" + " </app>\n"; +const char kUpdateManifestFooter[] = + "</gupdate>\n"; +const char kHostedAppID[] = "kbmnembihfiondgfjekmnmcbddelicoi"; +const char kHostedAppCRXPath[] = "extensions/hosted_app.crx"; +const char kHostedAppVersion[] = "0.1"; +const char kGoodExtensionID[] = "ldnnhddmnhbkjipkidpdiheffobcpfmf"; +const char kGoodExtensionPath[] = "extensions/good.crx"; +const char kGoodExtensionVersion[] = "1.0"; + +// Helper that serves extension update manifests to Chrome. +class TestingUpdateManifestProvider { + public: + + // Update manifests will be served at |relative_update_url|. + explicit TestingUpdateManifestProvider( + const std::string& relative_update_url); + ~TestingUpdateManifestProvider(); + + // When an update manifest is requested for the given extension |id|, indicate + // that |version| of the extension can be downloaded at |crx_url|. + void AddUpdate(const std::string& id, + const std::string& version, + const GURL& crx_url); + + // This method must be registered with the test's EmbeddedTestServer to start + // serving update manifests. + scoped_ptr<net::test_server::HttpResponse> HandleRequest( + const net::test_server::HttpRequest& request); + + private: + struct Update { + public: + Update(const std::string& version, const GURL& crx_url); + Update(); + + std::string version; + GURL crx_url; + }; + typedef std::map<std::string, Update> UpdateMap; + UpdateMap updates_; + + const std::string relative_update_url_; + + DISALLOW_COPY_AND_ASSIGN(TestingUpdateManifestProvider); +}; + +TestingUpdateManifestProvider::Update::Update(const std::string& version, + const GURL& crx_url) + : version(version), + crx_url(crx_url) { +} + +TestingUpdateManifestProvider::Update::Update() { +} + +TestingUpdateManifestProvider::TestingUpdateManifestProvider( + const std::string& relative_update_url) + : relative_update_url_(relative_update_url) { +} + +TestingUpdateManifestProvider::~TestingUpdateManifestProvider() { +} + +void TestingUpdateManifestProvider::AddUpdate(const std::string& id, + const std::string& version, + const GURL& crx_url) { + updates_[id] = Update(version, crx_url); +} + +scoped_ptr<net::test_server::HttpResponse> + TestingUpdateManifestProvider::HandleRequest( + const net::test_server::HttpRequest& request) { + const GURL url("http://localhost" + request.relative_url); + if (url.path() != relative_update_url_) + return scoped_ptr<net::test_server::HttpResponse>(); + + std::string content = kUpdateManifestHeader; + for (net::QueryIterator it(url); !it.IsAtEnd(); it.Advance()) { + if (it.GetKey() != "x") + continue; + // Extract the extension id from the subquery. Since GetValueForKeyInQuery() + // expects a complete URL, dummy scheme and host must be prepended. + std::string id; + net::GetValueForKeyInQuery(GURL("http://dummy?" + it.GetUnescapedValue()), + "id", &id); + UpdateMap::const_iterator entry = updates_.find(id); + if (entry != updates_.end()) { + content += base::StringPrintf(kUpdateManifestTemplate, + id.c_str(), + entry->second.crx_url.spec().c_str(), + entry->second.version.c_str()); + } + } + content += kUpdateManifestFooter; + scoped_ptr<net::test_server::BasicHttpResponse> + http_response(new net::test_server::BasicHttpResponse); + http_response->set_code(net::HTTP_OK); + http_response->set_content(content); + http_response->set_content_type("text/xml"); + return http_response.PassAs<net::test_server::HttpResponse>(); +} + +bool DoesInstallSuccessReferToId(const std::string& id, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + return content::Details<const extensions::InstalledExtensionInfo>(details)-> + extension->id() == id; +} + +bool DoesInstallFailureReferToId(const std::string& id, + const content::NotificationSource& source, + const content::NotificationDetails& details) { + return content::Details<const string16>(details)->find(UTF8ToUTF16(id)) != + string16::npos; +} } // namespace @@ -420,6 +554,92 @@ IN_PROC_BROWSER_TEST_F(DeviceLocalAccountTest, FullscreenDisallowed) { EXPECT_FALSE(browser_window->IsFullscreen()); } +IN_PROC_BROWSER_TEST_F(DeviceLocalAccountTest, ExtensionWhitelist) { + // Make it possible to force-install a hosted app and an extension. + ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady()); + TestingUpdateManifestProvider testing_update_manifest_provider( + kRelativeUpdateURL); + testing_update_manifest_provider.AddUpdate( + kHostedAppID, + kHostedAppVersion, + embedded_test_server()->GetURL(std::string("/") + kHostedAppCRXPath)); + testing_update_manifest_provider.AddUpdate( + kGoodExtensionID, + kGoodExtensionVersion, + embedded_test_server()->GetURL(std::string("/") + kGoodExtensionPath)); + embedded_test_server()->RegisterRequestHandler( + base::Bind(&TestingUpdateManifestProvider::HandleRequest, + base::Unretained(&testing_update_manifest_provider))); + + // Specify policy to force-install the hosted app and the extension. + em::StringList* forcelist = device_local_account_policy_.payload() + .mutable_extensioninstallforcelist()->mutable_value(); + forcelist->add_entries(base::StringPrintf( + "%s;%s", + kHostedAppID, + embedded_test_server()->GetURL(kRelativeUpdateURL).spec().c_str())); + forcelist->add_entries(base::StringPrintf( + "%s;%s", + kGoodExtensionID, + embedded_test_server()->GetURL(kRelativeUpdateURL).spec().c_str())); + + UploadAndInstallDeviceLocalAccountPolicy(); + AddPublicSessionToDevicePolicy(kAccountId1); + + // This observes the display name becoming available as this indicates + // device-local account policy is fully loaded, which is a prerequisite for + // successful login. + content::WindowedNotificationObserver( + chrome::NOTIFICATION_USER_LIST_CHANGED, + base::Bind(&DisplayNameMatches, user_id_1_, kDisplayName)).Wait(); + + // Wait for the login UI to be ready. + chromeos::LoginDisplayHostImpl* host = + reinterpret_cast<chromeos::LoginDisplayHostImpl*>( + chromeos::LoginDisplayHostImpl::default_host()); + ASSERT_TRUE(host); + chromeos::OobeUI* oobe_ui = host->GetOobeUI(); + ASSERT_TRUE(oobe_ui); + base::RunLoop run_loop; + const bool oobe_ui_ready = oobe_ui->IsJSReady(run_loop.QuitClosure()); + if (!oobe_ui_ready) + run_loop.Run(); + + // Ensure that the browser stays alive, even though no windows are opened + // during session start. + chrome::StartKeepAlive(); + + // Start listening for app/extension installation results. + content::WindowedNotificationObserver hosted_app_observer( + chrome::NOTIFICATION_EXTENSION_INSTALLED, + base::Bind(DoesInstallSuccessReferToId, kHostedAppID)); + content::WindowedNotificationObserver extension_observer( + chrome::NOTIFICATION_EXTENSION_INSTALL_ERROR, + base::Bind(DoesInstallFailureReferToId, kGoodExtensionID)); + + // Start login into the device-local account. + host->StartSignInScreen(); + chromeos::ExistingUserController* controller = + chromeos::ExistingUserController::current_controller(); + ASSERT_TRUE(controller); + controller->LoginAsPublicAccount(user_id_1_); + + // Wait for the hosted app installation to succeed and the extension + // installation to fail. + hosted_app_observer.Wait(); + extension_observer.Wait(); + + // Verify that the hosted app was installed. + Profile* profile = ProfileManager::GetDefaultProfile(); + ASSERT_TRUE(profile); + ExtensionService* extension_service = + extensions::ExtensionSystem::Get(profile)->extension_service(); + EXPECT_TRUE(extension_service->GetExtensionById(kHostedAppID, true)); + + // Verify that the extension was not installed. + EXPECT_FALSE(extension_service->GetExtensionById(kGoodExtensionID, true)); +} + class TermsOfServiceTest : public DeviceLocalAccountTest, public testing::WithParamInterface<bool> { }; diff --git a/chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc b/chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc index ec3dc2b..8312db9 100644 --- a/chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc +++ b/chrome/browser/chromeos/policy/device_local_account_policy_service_unittest.cc @@ -7,7 +7,6 @@ #include "base/bind.h" #include "base/bind_helpers.h" #include "base/callback.h" -#include "base/memory/scoped_ptr.h" #include "chrome/browser/chromeos/policy/device_local_account.h" #include "chrome/browser/chromeos/policy/device_local_account_policy_provider.h" #include "chrome/browser/chromeos/settings/cros_settings.h" @@ -80,13 +79,6 @@ class DeviceLocalAccountPolicyServiceTest POLICY_SCOPE_USER, Value::CreateBooleanValue(false), NULL); - scoped_ptr<base::ListValue> allowed_extension_types(new base::ListValue()); - allowed_extension_types->AppendString("hosted_app"); - expected_policy_map_.Set(key::kExtensionAllowedTypes, - POLICY_LEVEL_MANDATORY, - POLICY_SCOPE_USER, - allowed_extension_types.release(), - NULL); // Explicitly set value. expected_policy_map_.Set(key::kDisableSpdy, @@ -502,8 +494,6 @@ TEST_F(DeviceLocalAccountPolicyProviderTest, Policy) { set_value("Always"); device_local_account_policy_.payload().mutable_showlogoutbuttonintray()-> set_value(false); - device_local_account_policy_.payload().mutable_extensionallowedtypes()-> - mutable_value()->mutable_entries()->Clear(); device_local_account_policy_.Build(); device_settings_test_helper_.set_device_local_account_policy_blob( PolicyBuilder::kFakeUsername, device_local_account_policy_.GetBlob()); diff --git a/chrome/browser/chromeos/policy/device_local_account_policy_store.cc b/chrome/browser/chromeos/policy/device_local_account_policy_store.cc index f3914cf..898733a 100644 --- a/chrome/browser/chromeos/policy/device_local_account_policy_store.cc +++ b/chrome/browser/chromeos/policy/device_local_account_policy_store.cc @@ -86,7 +86,6 @@ void DeviceLocalAccountPolicyStore::UpdatePolicy( base::Value::CreateIntegerValue( chromeos::PowerPolicyController::ACTION_STOP_SESSION), NULL); - // Force the |ShelfAutoHideBehavior| policy to |Never|, ensuring that the ash // shelf does not auto-hide. policy_map_.Set(key::kShelfAutoHideBehavior, @@ -108,16 +107,6 @@ void DeviceLocalAccountPolicyStore::UpdatePolicy( POLICY_SCOPE_USER, Value::CreateBooleanValue(false), NULL); - // Restrict device-local accounts to hosted apps for now (i.e. no extensions, - // packaged apps etc.) for security/privacy reasons (i.e. we'd like to - // prevent the admin from stealing private information from random people). - scoped_ptr<base::ListValue> allowed_extension_types(new base::ListValue()); - allowed_extension_types->AppendString("hosted_app"); - policy_map_.Set(key::kExtensionAllowedTypes, - POLICY_LEVEL_MANDATORY, - POLICY_SCOPE_USER, - allowed_extension_types.release(), - NULL); status_ = STATUS_OK; NotifyStoreLoaded(); diff --git a/chrome/browser/extensions/extension_system.cc b/chrome/browser/extensions/extension_system.cc index ef79b60..348c5ee 100644 --- a/chrome/browser/extensions/extension_system.cc +++ b/chrome/browser/extensions/extension_system.cc @@ -48,6 +48,10 @@ #if defined(OS_CHROMEOS) #include "chrome/browser/app_mode/app_mode_utils.h" +#include "chrome/browser/chromeos/extensions/device_local_account_management_policy_provider.h" +#include "chrome/browser/chromeos/login/user.h" +#include "chrome/browser/chromeos/login/user_manager.h" +#include "chrome/browser/chromeos/policy/device_local_account.h" #include "chromeos/chromeos_switches.h" #include "chromeos/login/login_state.h" #endif @@ -107,7 +111,19 @@ void ExtensionSystemImpl::Shared::InitPrefs() { standard_management_policy_provider_.reset( new StandardManagementPolicyProvider(ExtensionPrefs::Get(profile_))); -#endif + +#if defined (OS_CHROMEOS) + const chromeos::User* user = chromeos::UserManager::Get()->GetActiveUser(); + policy::DeviceLocalAccount::Type device_local_account_type; + if (user && policy::IsDeviceLocalAccountUser(user->email(), + &device_local_account_type)) { + device_local_account_management_policy_provider_.reset( + new chromeos::DeviceLocalAccountManagementPolicyProvider( + device_local_account_type)); + } +#endif // defined (OS_CHROMEOS) + +#endif // defined(ENABLE_EXTENSIONS) } void ExtensionSystemImpl::Shared::RegisterManagementPolicyProviders() { @@ -116,7 +132,15 @@ void ExtensionSystemImpl::Shared::RegisterManagementPolicyProviders() { DCHECK(standard_management_policy_provider_.get()); management_policy_->RegisterProvider( standard_management_policy_provider_.get()); -#endif + +#if defined (OS_CHROMEOS) + if (device_local_account_management_policy_provider_) { + management_policy_->RegisterProvider( + device_local_account_management_policy_provider_.get()); + } +#endif // defined (OS_CHROMEOS) + +#endif // defined(ENABLE_EXTENSIONS) } void ExtensionSystemImpl::Shared::Init(bool extensions_enabled) { diff --git a/chrome/browser/extensions/extension_system.h b/chrome/browser/extensions/extension_system.h index 8a40070..c88fc74 100644 --- a/chrome/browser/extensions/extension_system.h +++ b/chrome/browser/extensions/extension_system.h @@ -18,6 +18,12 @@ class ExtensionProcessManager; class ExtensionService; class Profile; +#if defined(OS_CHROMEOS) +namespace chromeos { +class DeviceLocalAccountManagementPolicyProvider; +} +#endif // defined(OS_CHROMEOS) + namespace extensions { class Blacklist; class ErrorConsole; @@ -213,6 +219,11 @@ class ExtensionSystemImpl : public ExtensionSystem { scoped_ptr<ExtensionWarningBadgeService> extension_warning_badge_service_; scoped_ptr<ErrorConsole> error_console_; +#if defined(OS_CHROMEOS) + scoped_ptr<chromeos::DeviceLocalAccountManagementPolicyProvider> + device_local_account_management_policy_provider_; +#endif + OneShotEvent ready_; }; diff --git a/chrome/browser/policy/browser_policy_connector.cc b/chrome/browser/policy/browser_policy_connector.cc index d4d0f61..9132f43 100644 --- a/chrome/browser/policy/browser_policy_connector.cc +++ b/chrome/browser/policy/browser_policy_connector.cc @@ -415,7 +415,7 @@ UserAffiliation BrowserPolicyConnector::GetUserAffiliation( if (install_attributes_ && (gaia::ExtractDomainName(gaia::CanonicalizeEmail(user_name)) == install_attributes_->GetDomain() || - policy::IsDeviceLocalAccountUser(user_name))) { + policy::IsDeviceLocalAccountUser(user_name, NULL))) { return USER_AFFILIATION_MANAGED; } #endif diff --git a/chrome/chrome_browser_chromeos.gypi b/chrome/chrome_browser_chromeos.gypi index 71505cd..88619be 100644 --- a/chrome/chrome_browser_chromeos.gypi +++ b/chrome/chrome_browser_chromeos.gypi @@ -298,6 +298,8 @@ 'browser/chromeos/enrollment_dialog_view.h', 'browser/chromeos/extensions/default_app_order.cc', 'browser/chromeos/extensions/default_app_order.h', + 'browser/chromeos/extensions/device_local_account_management_policy_provider.cc', + 'browser/chromeos/extensions/device_local_account_management_policy_provider.h', 'browser/chromeos/extensions/echo_private_api.cc', 'browser/chromeos/extensions/echo_private_api.h', 'browser/chromeos/extensions/external_cache.cc', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index 9742249..98f759f 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -655,6 +655,7 @@ 'browser/chromeos/file_manager/mounted_disk_monitor_unittest.cc', 'browser/chromeos/file_manager/url_util_unittest.cc', 'browser/chromeos/file_manager/volume_manager_unittest.cc', + 'browser/chromeos/extensions/device_local_account_management_policy_provider_unittest.cc', 'browser/chromeos/extensions/wallpaper_private_api_unittest.cc', 'browser/chromeos/external_metrics_unittest.cc', 'browser/chromeos/fileapi/file_access_permissions_unittest.cc', diff --git a/content/public/test/test_utils.cc b/content/public/test/test_utils.cc index 92c6fdc..989437c 100644 --- a/content/public/test/test_utils.cc +++ b/content/public/test/test_utils.cc @@ -67,6 +67,17 @@ void ScriptCallback::ResultCallback(const base::Value* result) { base::MessageLoop::current()->Quit(); } +// Adapter that makes a WindowedNotificationObserver::ConditionTestCallback from +// a WindowedNotificationObserver::ConditionTestCallbackWithoutSourceAndDetails +// by ignoring the notification source and details. +bool IgnoreSourceAndDetails( + const WindowedNotificationObserver:: + ConditionTestCallbackWithoutSourceAndDetails& callback, + const NotificationSource& source, + const NotificationDetails& details) { + return callback.Run(); +} + } // namespace void RunMessageLoop() { @@ -186,6 +197,16 @@ WindowedNotificationObserver::WindowedNotificationObserver( AddNotificationType(notification_type, source_); } +WindowedNotificationObserver::WindowedNotificationObserver( + int notification_type, + const ConditionTestCallbackWithoutSourceAndDetails& callback) + : seen_(false), + running_(false), + callback_(base::Bind(&IgnoreSourceAndDetails, callback)), + source_(NotificationService::AllSources()) { + registrar_.Add(this, notification_type, source_); +} + WindowedNotificationObserver::~WindowedNotificationObserver() {} void WindowedNotificationObserver::AddNotificationType( @@ -210,7 +231,7 @@ void WindowedNotificationObserver::Observe( const NotificationDetails& details) { source_ = source; details_ = details; - if (!callback_.is_null() && !callback_.Run()) + if (!callback_.is_null() && !callback_.Run(source, details)) return; seen_ = true; diff --git a/content/public/test/test_utils.h b/content/public/test/test_utils.h index 0b8fa76..c1aa189 100644 --- a/content/public/test/test_utils.h +++ b/content/public/test/test_utils.h @@ -100,7 +100,8 @@ class MessageLoopRunner : public base::RefCounted<MessageLoopRunner> { // If the callback returns |true|, the condition is met. Otherwise, the // condition is not yet met and the callback will be invoked again every time a // notification of the expected type is received until the callback returns -// |true|. +// |true|. For convenience, two callback types are defined, one that is provided +// with the notification source and details, and one that is not. // // This helper class exists to avoid the following common pattern in tests: // PerformAction() @@ -116,8 +117,14 @@ class MessageLoopRunner : public base::RefCounted<MessageLoopRunner> { class WindowedNotificationObserver : public NotificationObserver { public: // Callback invoked on notifications. Should return |true| when the condition - // being waited for is met. - typedef base::Callback<bool(void)> ConditionTestCallback; + // being waited for is met. For convenience, there is a choice between two + // callback types, one that is provided with the notification source and + // details, and one that is not. + typedef base::Callback<bool(const NotificationSource&, + const NotificationDetails&)> + ConditionTestCallback; + typedef base::Callback<bool(void)> + ConditionTestCallbackWithoutSourceAndDetails; // Set up to wait for a simple condition. The condition is met when a // notification of the given |notification_type| from the given |source| is @@ -131,6 +138,9 @@ class WindowedNotificationObserver : public NotificationObserver { // of |notification_type| from any source is received. WindowedNotificationObserver(int notification_type, const ConditionTestCallback& callback); + WindowedNotificationObserver( + int notification_type, + const ConditionTestCallbackWithoutSourceAndDetails& callback); virtual ~WindowedNotificationObserver(); diff --git a/net/base/url_util.cc b/net/base/url_util.cc index c18fd17..a7c89e9 100644 --- a/net/base/url_util.cc +++ b/net/base/url_util.cc @@ -4,6 +4,9 @@ #include "net/base/url_util.h" +#include <utility> + +#include "base/logging.h" #include "base/strings/string_piece.h" #include "net/base/escape.h" #include "url/gurl.h" @@ -68,30 +71,66 @@ GURL AppendOrReplaceQueryParameter(const GURL& url, return url.ReplaceComponents(replacements); } +QueryIterator::QueryIterator(const GURL& url) + : url_(url), + at_end_(!url.is_valid()) { + if (!at_end_) { + query_ = url.parsed_for_possibly_invalid_spec().query; + Advance(); + } +} + +QueryIterator::~QueryIterator() { +} + +std::string QueryIterator::GetKey() const { + DCHECK(!at_end_); + if (key_.is_nonempty()) + return url_.spec().substr(key_.begin, key_.len); + return std::string(); +} + +std::string QueryIterator::GetValue() const { + DCHECK(!at_end_); + if (value_.is_nonempty()) + return url_.spec().substr(value_.begin, value_.len); + return std::string(); +} + +const std::string& QueryIterator::GetUnescapedValue() { + DCHECK(!at_end_); + if (value_.is_nonempty() && unescaped_value_.empty()) { + unescaped_value_ = UnescapeURLComponent( + GetValue(), + UnescapeRule::SPACES | + UnescapeRule::URL_SPECIAL_CHARS | + UnescapeRule::REPLACE_PLUS_WITH_SPACE); + } + return unescaped_value_; +} + +bool QueryIterator::IsAtEnd() const { + return at_end_; +} + +void QueryIterator::Advance() { + DCHECK (!at_end_); + key_.reset(); + value_.reset(); + unescaped_value_.clear(); + at_end_ = !url_parse::ExtractQueryKeyValue(url_.spec().c_str(), + &query_, + &key_, + &value_); +} + bool GetValueForKeyInQuery(const GURL& url, const std::string& search_key, std::string* out_value) { - if (!url.is_valid()) - return false; - - url_parse::Component query = url.parsed_for_possibly_invalid_spec().query; - url_parse::Component key, value; - while (url_parse::ExtractQueryKeyValue( - url.spec().c_str(), &query, &key, &value)) { - if (key.is_nonempty()) { - std::string key_string = url.spec().substr(key.begin, key.len); - if (key_string == search_key) { - if (value.is_nonempty()) { - *out_value = UnescapeURLComponent( - url.spec().substr(value.begin, value.len), - UnescapeRule::SPACES | - UnescapeRule::URL_SPECIAL_CHARS | - UnescapeRule::REPLACE_PLUS_WITH_SPACE); - } else { - *out_value = ""; - } - return true; - } + for (QueryIterator it(url); !it.IsAtEnd(); it.Advance()) { + if (it.GetKey() == search_key) { + *out_value = it.GetUnescapedValue(); + return true; } } return false; diff --git a/net/base/url_util.h b/net/base/url_util.h index 5d5e58b..83d0356 100644 --- a/net/base/url_util.h +++ b/net/base/url_util.h @@ -7,7 +7,9 @@ #include <string> +#include "base/compiler_specific.h" #include "net/base/net_export.h" +#include "url/url_parse.h" class GURL; @@ -44,6 +46,30 @@ NET_EXPORT GURL AppendOrReplaceQueryParameter(const GURL& url, const std::string& name, const std::string& value); +// Iterates over the key-value pairs in the query portion of |url|. +class NET_EXPORT QueryIterator { + public: + explicit QueryIterator(const GURL& url); + ~QueryIterator(); + + std::string GetKey() const; + std::string GetValue() const; + const std::string& GetUnescapedValue(); + + bool IsAtEnd() const; + void Advance(); + + private: + const GURL& url_; + url_parse::Component query_; + bool at_end_; + url_parse::Component key_; + url_parse::Component value_; + std::string unescaped_value_; + + DISALLOW_COPY_AND_ASSIGN(QueryIterator); +}; + // Looks for |search_key| in the query portion of |url|. Returns true if the // key is found and sets |out_value| to the unescaped value for the key. // Returns false if the key is not found. diff --git a/net/base/url_util_unittest.cc b/net/base/url_util_unittest.cc index 83ac258..158e274 100644 --- a/net/base/url_util_unittest.cc +++ b/net/base/url_util_unittest.cc @@ -108,5 +108,56 @@ TEST(UrlUtilTest, GetValueForKeyInQueryInvalidURL) { EXPECT_FALSE(GetValueForKeyInQuery(url, "test", &value)); } +TEST(UrlUtilTest, ParseQuery) { + const GURL url("http://example.com/path?name=value&boolParam&" + "url=http://test.com/q?n1%3Dv1%26n2&" + "multikey=value1&multikey=value2&multikey"); + QueryIterator it(url); + + ASSERT_FALSE(it.IsAtEnd()); + EXPECT_EQ("name", it.GetKey()); + EXPECT_EQ("value", it.GetValue()); + EXPECT_EQ("value", it.GetUnescapedValue()); + it.Advance(); + + ASSERT_FALSE(it.IsAtEnd()); + EXPECT_EQ("boolParam", it.GetKey()); + EXPECT_EQ("", it.GetValue()); + EXPECT_EQ("", it.GetUnescapedValue()); + it.Advance(); + + ASSERT_FALSE(it.IsAtEnd()); + EXPECT_EQ("url", it.GetKey()); + EXPECT_EQ("http://test.com/q?n1%3Dv1%26n2", it.GetValue()); + EXPECT_EQ("http://test.com/q?n1=v1&n2", it.GetUnescapedValue()); + it.Advance(); + + ASSERT_FALSE(it.IsAtEnd()); + EXPECT_EQ("multikey", it.GetKey()); + EXPECT_EQ("value1", it.GetValue()); + EXPECT_EQ("value1", it.GetUnescapedValue()); + it.Advance(); + + ASSERT_FALSE(it.IsAtEnd()); + EXPECT_EQ("multikey", it.GetKey()); + EXPECT_EQ("value2", it.GetValue()); + EXPECT_EQ("value2", it.GetUnescapedValue()); + it.Advance(); + + ASSERT_FALSE(it.IsAtEnd()); + EXPECT_EQ("multikey", it.GetKey()); + EXPECT_EQ("", it.GetValue()); + EXPECT_EQ("", it.GetUnescapedValue()); + it.Advance(); + + EXPECT_TRUE(it.IsAtEnd()); +} + +TEST(UrlUtilTest, ParseQueryInvalidURL) { + const GURL url("http://%01/?test"); + QueryIterator it(url); + EXPECT_TRUE(it.IsAtEnd()); +} + } // namespace } // namespace net |