blob: 11f4aee472df03e2eb1e97640b854feac3ff1007 [file] [log] [blame] [edit]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/files/file_path.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api/messaging/messaging_delegate.h"
#include "extensions/common/extension.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
using extensions::mojom::ManifestLocation;
namespace policy {
namespace {
// Utility for waiting until the dev-mode controls are visible/hidden
// Uses a MutationObserver on the attributes of the DOM element.
void WaitForExtensionsDevModeControlsVisibility(
content::WebContents* contents,
const char* dev_controls_accessor_js,
const char* dev_controls_visibility_check_js,
bool expected_visible) {
ASSERT_TRUE(content::ExecJs(
contents,
base::StringPrintf(
"var screenElement = %s;"
"new Promise(resolve => {"
" function SendReplyIfAsExpected() {"
" var is_visible = %s;"
" if (is_visible != %s)"
" return false;"
" observer.disconnect();"
" resolve(true);"
" return true;"
" }"
" var observer = new MutationObserver(SendReplyIfAsExpected);"
" if (!SendReplyIfAsExpected()) {"
" var options = { 'attributes': true };"
" observer.observe(screenElement, options);"
" }"
"});",
dev_controls_accessor_js, dev_controls_visibility_check_js,
(expected_visible ? "true" : "false"))));
}
// Utility to get a PolicyMap for setting the DeveloperToolsAvailability policy
// to a given value.
PolicyMap MakeDeveloperToolsAvailabilityMap(int value) {
PolicyMap policies;
policies.Set(key::kDeveloperToolsAvailability, POLICY_LEVEL_MANDATORY,
POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD, base::Value(value),
nullptr);
return policies;
}
// Navigates the current tab of the browser to the given URL without any
// waiting after the navigation is triggered. Note: the
// ui_test_utils::BROWSER_TEST_NO_WAIT flag passed in results this returning
// right after the Browser::OpenURL() call without waiting for any load
// events.
void NavigateToURLNoWait(Browser* browser, const GURL& url) {
ui_test_utils::NavigateToURLWithDisposition(
browser, url, WindowOpenDisposition::CURRENT_TAB,
ui_test_utils::BROWSER_TEST_NO_WAIT);
}
// Utility to navigate the current tab of the browser to the specified page and
// then kill it using chrome://kill, verifying that the page ends up crashed.
void VerifyPageAllowsKill(Browser* browser, const GURL& url) {
SCOPED_TRACE(base::StringPrintf("Verifying url allows kill: '%s'",
url.spec().c_str()));
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser, url));
{
content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes;
content::RenderProcessHostWatcher exit_observer(
browser->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame()
->GetProcess(),
content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);
ASSERT_TRUE(
ui_test_utils::NavigateToURL(browser, GURL(blink::kChromeUIKillURL)));
exit_observer.Wait();
// The kill url will have left a hanging pending entry on the
// NavigationController and the contents will be marked as having crashed.
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
EXPECT_TRUE(web_contents->GetController().GetPendingEntry());
EXPECT_TRUE(web_contents->IsCrashed());
EXPECT_FALSE(exit_observer.did_exit_normally());
}
}
// Utility to navigate the current tab of the browser to the specified page and
// then attempt to kill it using chrome://kill, verifying that the kill is
// blocked before any navigation is started.
void VerifyPageBlocksKill(Browser* browser, const GURL& url) {
SCOPED_TRACE(base::StringPrintf("Verifying url blocks kill: '%s'",
url.spec().c_str()));
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser, url));
// We expect the debug URL to be blocked synchronously, so we don't have to
// wait for a load stop here. Afterwards we verify nothing has happened by
// checking there is no pending entry on the NavigationController (indicating
// no navigation was started) and that the contents has not crashed.
NavigateToURLNoWait(browser, GURL(blink::kChromeUIKillURL));
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
EXPECT_FALSE(web_contents->GetController().GetPendingEntry());
EXPECT_FALSE(web_contents->IsCrashed());
}
// Utility to navigate the current tab of the browser to the specified page and
// return true if a javascript URL can be run on it, false otherwise.
bool PageAllowsJavascriptURL(Browser* browser, const GURL& url) {
SCOPED_TRACE(base::StringPrintf("Checking url allows javascript URLs: '%s'",
url.spec().c_str()));
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser, url));
content::WebContents* web_contents =
browser->tab_strip_model()->GetActiveWebContents();
const std::u16string original_title = web_contents->GetTitle();
const GURL javascript_url("javascript:void(document.title='Modified Title')");
NavigateToURLNoWait(browser, javascript_url);
// Run another script we can wait on to ensure the javascript URL will have
// processed if it was going to.
EXPECT_EQ(true, content::EvalJs(web_contents, "true"));
// Check if the title was changed and return true if so.
if (web_contents->GetTitle() != original_title) {
EXPECT_EQ(u"Modified Title", web_contents->GetTitle());
return true;
}
EXPECT_NE(u"Modified Title", web_contents->GetTitle());
return false;
}
} // namespace
IN_PROC_BROWSER_TEST_F(PolicyTest, DeveloperToolsDisabledByLegacyPolicy) {
// Verifies that access to the developer tools can be disabled by setting the
// legacy DeveloperToolsDisabled policy.
// Open devtools.
EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
content::WebContents* contents =
browser()->tab_strip_model()->GetActiveWebContents();
DevToolsWindow* devtools_window =
DevToolsWindow::GetInstanceForInspectedWebContents(contents);
EXPECT_TRUE(devtools_window);
// Disable devtools via policy.
PolicyMap policies;
policies.Set(key::kDeveloperToolsDisabled, POLICY_LEVEL_MANDATORY,
POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD, base::Value(true),
nullptr);
content::WebContentsDestroyedWatcher close_observer(
DevToolsWindowTesting::Get(devtools_window)->main_web_contents());
UpdateProviderPolicy(policies);
// wait for devtools close
close_observer.Wait();
// The existing devtools window should have closed.
EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
// And it's not possible to open it again.
EXPECT_FALSE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
}
IN_PROC_BROWSER_TEST_F(PolicyTest,
DeveloperToolsDisabledByDeveloperToolsAvailability) {
// Verifies that access to the developer tools can be disabled by setting the
// DeveloperToolsAvailability policy.
// Open devtools.
EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
content::WebContents* contents =
browser()->tab_strip_model()->GetActiveWebContents();
DevToolsWindow* devtools_window =
DevToolsWindow::GetInstanceForInspectedWebContents(contents);
EXPECT_TRUE(devtools_window);
content::WebContentsDestroyedWatcher close_observer(
DevToolsWindowTesting::Get(devtools_window)->main_web_contents());
// Disable devtools via policy.
UpdateProviderPolicy(
MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));
// wait for devtools close
close_observer.Wait();
// The existing devtools window should have closed.
EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
// And it's not possible to open it again.
EXPECT_FALSE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
}
// Test for https://b/263040629
IN_PROC_BROWSER_TEST_F(PolicyTest, AvailabilityWins) {
// DeveloperToolsDisabled is true, but DeveloperToolsAvailability wins.
PolicyMap policies;
policies.Set(key::kDeveloperToolsAvailability, POLICY_LEVEL_MANDATORY,
POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
base::Value(1 /* DeveloperToolsAllowed */), nullptr);
policies.Set(key::kDeveloperToolsDisabled, POLICY_LEVEL_MANDATORY,
POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD, base::Value(true),
nullptr);
UpdateProviderPolicy(policies);
EXPECT_TRUE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
content::WebContents* contents =
browser()->tab_strip_model()->GetActiveWebContents();
DevToolsWindow* devtools_window =
DevToolsWindow::GetInstanceForInspectedWebContents(contents);
EXPECT_TRUE(devtools_window);
// Clearing DeveloperToolsAvailability leaves behind
// DeveloperToolsDisabled, so the DevTools window gets closed.
content::WebContentsDestroyedWatcher close_observer(
DevToolsWindowTesting::Get(devtools_window)->main_web_contents());
policies.Erase(key::kDeveloperToolsAvailability);
UpdateProviderPolicy(policies);
// wait for devtools close
close_observer.Wait();
// The existing devtools window should have closed.
EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
// And it's not possible to open it again.
EXPECT_FALSE(chrome::ExecuteCommand(browser(), IDC_DEV_TOOLS));
EXPECT_FALSE(DevToolsWindow::GetInstanceForInspectedWebContents(contents));
}
IN_PROC_BROWSER_TEST_F(PolicyTest,
ViewSourceDisabledByDeveloperToolsAvailability) {
// Verifies that entry points to ViewSource can be disabled by setting the
// DeveloperToolsAvailability policy.
// Disable devtools via policy.
UpdateProviderPolicy(
MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));
// Verify that it's not possible to ViewSource.
EXPECT_FALSE(chrome::ExecuteCommand(browser(), IDC_VIEW_SOURCE));
}
IN_PROC_BROWSER_TEST_F(PolicyTest, DeveloperToolsDisabledExtensionsDevMode) {
// Verifies that when DeveloperToolsDisabled policy is set, the "dev mode"
// in chrome://extensions is actively turned off and the checkbox
// is disabled.
// Note: We don't test the indicator as it is tested in the policy pref test
// for kDeveloperToolsDisabled and kDeveloperToolsAvailability.
// This test depends on the following helper methods to locate the DOM
// elements to be tested.
const char define_helpers_js[] =
R"(function getToolbar() {
const manager = document.querySelector('extensions-manager');
return manager.shadowRoot.querySelector('extensions-toolbar');
}
function getToggle() {
return getToolbar().$.devMode;
}
function getControls() {
return getToolbar().$.devDrawer;
}
)";
const char toggle_dev_mode_accessor_js[] = "getToggle()";
const char dev_controls_accessor_js[] = "getControls()";
const char dev_controls_visibility_check_js[] =
"getControls().hasAttribute('expanded')";
// Navigate to the extensions frame and enabled "Developer mode"
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), GURL(chrome::kChromeUIExtensionsURL)));
content::WebContents* contents =
browser()->tab_strip_model()->GetActiveWebContents();
EXPECT_TRUE(content::ExecJs(contents, std::string(define_helpers_js)));
EXPECT_TRUE(content::ExecJs(
contents, base::StringPrintf("domAutomationController.send(%s.click());",
toggle_dev_mode_accessor_js)));
WaitForExtensionsDevModeControlsVisibility(contents, dev_controls_accessor_js,
dev_controls_visibility_check_js,
true);
// Disable devtools via policy.
UpdateProviderPolicy(
MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));
// Expect devcontrols to be hidden now...
WaitForExtensionsDevModeControlsVisibility(contents, dev_controls_accessor_js,
dev_controls_visibility_check_js,
false);
// ... and checkbox is disabled
EXPECT_EQ(true, content::EvalJs(contents, base::StringPrintf(
"%s.hasAttribute('disabled')",
toggle_dev_mode_accessor_js)));
}
// Verifies debug URLs, specifically chrome://kill and javascript URLs, are
// blocked or allowed for different pages depending on the
// DeveloperToolsAvailability policy setting. Note: javascript URLs are always
// blocked on extension schemes, regardless of the policy setting.
// TODO(crbug.com/40064953): The loading of a force installed extension in this
// test runs into an issue on branded Windows builders.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING) && BUILDFLAG(IS_WIN)
#define MAYBE_DebugURLsDisabledByDeveloperToolsAvailability \
DISABLED_DebugURLsDisabledByDeveloperToolsAvailability
#else
#define MAYBE_DebugURLsDisabledByDeveloperToolsAvailability \
DebugURLsDisabledByDeveloperToolsAvailability
#endif
IN_PROC_BROWSER_TEST_F(PolicyTest,
MAYBE_DebugURLsDisabledByDeveloperToolsAvailability) {
// Get a url for a standard web page.
ASSERT_TRUE(embedded_test_server()->Start());
GURL tab_url(embedded_test_server()->GetURL("/empty.html"));
// Get a url for a force installed extension.
base::FilePath crx_path(ui_test_utils::GetTestFilePath(
base::FilePath().AppendASCII("devtools").AppendASCII("extensions"),
base::FilePath().AppendASCII("options.crx")));
extensions::ChromeTestExtensionLoader loader(browser()->profile());
// TODO(crbug.com/40269105): We shouldn't need to ignore manifest warnings
// here, but there's an issue related to the _metadata folder added for
// content verification when force-installing an off-store crx in a branded
// build, which produces an install warning.
loader.set_ignore_manifest_warnings(true);
loader.set_location(ManifestLocation::kExternalPolicyDownload);
scoped_refptr<const extensions::Extension> extension =
loader.LoadExtension(crx_path);
ASSERT_TRUE(extension);
GURL extension_url("chrome-extension://" + extension->id() + "/options.html");
// The default for DeveloperToolsAvailability is to disallow for force
// installed extensions. Even though that is already the value, we set it here
// to be explicit about what the value currently is.
// With this setting force installed extension should block the debug and
// javascript URLs and but normal pages should allow them.
UpdateProviderPolicy(MakeDeveloperToolsAvailabilityMap(
0 /* DeveloperToolsDisallowedForForceInstalledExtensions */));
{
SCOPED_TRACE(
"Testing DeveloperToolsDisallowedForForceInstalledExtensions policy "
"setting");
VerifyPageAllowsKill(browser(), tab_url);
EXPECT_TRUE(PageAllowsJavascriptURL(browser(), tab_url));
VerifyPageBlocksKill(browser(), extension_url);
EXPECT_FALSE(PageAllowsJavascriptURL(browser(), extension_url));
}
// When the policy is set to always allow Devtools all the pages should allow
// debug URLs to be used, but javascript URLs will still be blocked on any
// extension schemes.
UpdateProviderPolicy(
MakeDeveloperToolsAvailabilityMap(1 /* DeveloperToolsAllowed */));
{
SCOPED_TRACE("Testing DeveloperToolsAllowed policy setting");
VerifyPageAllowsKill(browser(), tab_url);
EXPECT_TRUE(PageAllowsJavascriptURL(browser(), tab_url));
VerifyPageAllowsKill(browser(), extension_url);
EXPECT_FALSE(PageAllowsJavascriptURL(browser(), extension_url));
}
// When the policy is set to always disallow Devtools all the pages should
// block debug and javascript URLs.
UpdateProviderPolicy(
MakeDeveloperToolsAvailabilityMap(2 /* DeveloperToolsDisallowed */));
{
SCOPED_TRACE("Testing DeveloperToolsDisallowed policy setting");
VerifyPageBlocksKill(browser(), tab_url);
EXPECT_FALSE(PageAllowsJavascriptURL(browser(), tab_url));
VerifyPageBlocksKill(browser(), extension_url);
EXPECT_FALSE(PageAllowsJavascriptURL(browser(), extension_url));
}
}
} // namespace policy