blob: 8d73de720dae6fe17506fb5f5efd269b410d4c69 [file] [log] [blame]
// Copyright 2025 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/smart_card/chromeos_smart_card_delegate.h"
#include <optional>
#include "ash/constants/ash_switches.h"
#include "base/check_deref.h"
#include "base/memory/raw_ref.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chrome/browser/smart_card/smart_card_permission_context.h"
#include "chrome/browser/smart_card/smart_card_permission_context_factory.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/testing_profile.h"
#include "components/content_settings/browser/page_specific_content_settings.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/content_settings_types.mojom-shared.h"
#include "components/tabs/public/tab_interface.h"
#include "components/user_manager/user_names.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/smart_card_delegate.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_base.h"
#include "third_party/blink/public/common/features.h"
namespace {
constexpr char kDummyReader[] = "Dummy Reader";
} // namespace
class ChromeOsSmartCardDelegateBrowserTest
: public web_app::IsolatedWebAppBrowserTestHarness {
public:
void SetUpOnMainThread() override {
IsolatedWebAppBrowserTestHarness::SetUpOnMainThread();
app_ = web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kSmartCard))
.BuildBundle();
app_frame_ = OpenApp(app_->InstallChecked(profile()).app_id());
ASSERT_TRUE(app_frame_);
}
void TearDownOnMainThread() override {
app_.reset();
app_frame_ = nullptr;
IsolatedWebAppBrowserTestHarness::TearDownOnMainThread();
}
base::Time GetLastUsed(content::RenderFrameHost& rfh) {
return content_settings::PageSpecificContentSettings::GetForFrame(&rfh)
->GetLastUsedTime(
content_settings::mojom::ContentSettingsType::SMART_CARD_GUARD);
}
bool IsInUse(content::RenderFrameHost& rfh) {
return content_settings::PageSpecificContentSettings::GetForFrame(&rfh)
->IsInUse(
content_settings::mojom::ContentSettingsType::SMART_CARD_GUARD);
}
content::SmartCardDelegate* GetSmartCardDelegate() {
return content::GetContentClientForTesting()
->browser()
->GetSmartCardDelegate();
}
void GrantReaderPermission() {
SmartCardPermissionContextFactory::GetForProfile(*profile())
.GrantPersistentReaderPermission(app_frame_->GetLastCommittedOrigin(),
kDummyReader);
}
void GrantEphemeralReaderPermission() {
SmartCardPermissionContextFactory::GetForProfile(*profile())
.GrantEphemeralReaderPermission(app_frame_->GetLastCommittedOrigin(),
kDummyReader);
}
bool HasReaderPermission(const url::Origin& origin,
const std::string& reader) {
return SmartCardPermissionContextFactory::GetForProfile(*profile())
.HasReaderPermission(origin, reader);
}
protected:
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app_;
raw_ptr<content::RenderFrameHost> app_frame_ = nullptr;
private:
base::test::ScopedFeatureList scoped_feature_list_{
blink::features::kSmartCard};
};
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
NotifyConnectionUsed) {
auto before = base::Time::Now();
ASSERT_GT(before, GetLastUsed(*app_frame_));
GetSmartCardDelegate()->NotifyConnectionUsed(*app_frame_);
ASSERT_LE(before, GetLastUsed(*app_frame_));
ASSERT_TRUE(IsInUse(*app_frame_));
}
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
NotifyLastConnectionLost) {
ASSERT_FALSE(IsInUse(*app_frame_));
GetSmartCardDelegate()->NotifyConnectionUsed(*app_frame_);
ASSERT_TRUE(IsInUse(*app_frame_));
GetSmartCardDelegate()->NotifyLastConnectionLost(*app_frame_);
ASSERT_FALSE(IsInUse(*app_frame_));
}
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
ReconnectionInTheBackgroundImpossible) {
GrantReaderPermission();
auto& pscs = CHECK_DEREF(
content_settings::PageSpecificContentSettings::GetForFrame(app_frame_));
// Set the last used time past the reconnection deadline.
//
// Depending on real time in browser tests might not be generally a good idea,
// but this one will be stable as long as test execution doesn't travel back
// in time. If it does, we might have bigger problems than
// instability here.
pscs.set_last_used_time_for_testing(ContentSettingsType::SMART_CARD_GUARD,
base::Time::Now() -
base::Seconds(16));
// Hiding takes focus from the window.
content::WebContents::FromRenderFrameHost(app_frame_)
->GetTopLevelNativeWindow()
->Hide();
// Window does not have focus and the last connection was more than
// `kSmartCardAllowedReconnectTime` ago. Hence, connection is not possible.
EXPECT_FALSE(
GetSmartCardDelegate()->HasReaderPermission(*app_frame_, kDummyReader));
}
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
ReconnectionInTheBackgroundPossible) {
GrantReaderPermission();
auto& pscs = CHECK_DEREF(
content_settings::PageSpecificContentSettings::GetForFrame(app_frame_));
// Set the last used time within the reconnection deadline (in the future).
//
// Depending on real time in browser tests might not be generally a good idea,
// but this one will be stable as long as test execution from here takes less
// than a minute. If it takes longer, we might have bigger problems than
// instability here.
pscs.set_last_used_time_for_testing(ContentSettingsType::SMART_CARD_GUARD,
base::Time::Now() + base::Minutes(1));
// Hiding takes focus from the window.
content::WebContents::FromRenderFrameHost(app_frame_)
->GetTopLevelNativeWindow()
->Hide();
// Window does not have focus but the last connection was less than
// `kSmartCardAllowedReconnectTime` ago (probably still in the future). Hence,
// connection is deemed a reconnection and is possible.
EXPECT_TRUE(
GetSmartCardDelegate()->HasReaderPermission(*app_frame_, kDummyReader));
}
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
ReconnectionInTheForegroundPossible) {
GrantReaderPermission();
auto& pscs = CHECK_DEREF(
content_settings::PageSpecificContentSettings::GetForFrame(app_frame_));
// Set the last used time past the reconnection deadline.
//
// Depending on real time in browser tests might not be generally a good idea,
// but this one will be stable as long as test execution doesn't travel back
// in time. If it does, we might have bigger problems than
// instability here.
pscs.set_last_used_time_for_testing(ContentSettingsType::SMART_CARD_GUARD,
base::Time::Now() - base::Seconds(16));
// Long time from last connection should not have impact on connection
// possibility as long as the window has focus.
EXPECT_TRUE(
GetSmartCardDelegate()->HasReaderPermission(*app_frame_, kDummyReader));
}
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
PermissionRequestInTheBackgroundImpossible) {
GrantReaderPermission();
// Hiding takes focus from the window.
content::WebContents::FromRenderFrameHost(app_frame_)
->GetTopLevelNativeWindow()
->Hide();
base::test::TestFuture<bool> got_permission;
// This should immediately return false, not even trying to display any
// prompts.
GetSmartCardDelegate()->RequestReaderPermission(*app_frame_, kDummyReader,
got_permission.GetCallback());
EXPECT_FALSE(got_permission.Get());
}
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTest,
ClosingWindowClearsEphemeralPermissions) {
GrantEphemeralReaderPermission();
const auto origin = app_frame_->GetLastCommittedOrigin();
EXPECT_TRUE(HasReaderPermission(origin, kDummyReader));
auto* tab_interface = tabs::TabInterface::GetFromContents(
content::WebContents::FromRenderFrameHost(app_frame_));
// Not resetting this causes dangling raw pointer error when closing tab.
app_frame_ = nullptr;
tab_interface->Close();
EXPECT_FALSE(HasReaderPermission(origin, kDummyReader));
}
class ChromeOsSmartCardDelegateBrowserTestGuestMode
: public web_app::IsolatedWebAppBrowserTestHarness {
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(ash::switches::kGuestSession);
command_line->AppendSwitchASCII(ash::switches::kLoginUser,
user_manager::kGuestUserName);
command_line->AppendSwitchASCII(ash::switches::kLoginProfile,
TestingProfile::kTestUserProfileDir);
command_line->AppendSwitch(switches::kIncognito);
}
};
// This is essentially just a sanity check of whether the
// SmartCardPermissionContext will be created successfully in guest mode.
// Probably to be deleted as a part of this: TODO(crbug.com/454152416)
IN_PROC_BROWSER_TEST_F(ChromeOsSmartCardDelegateBrowserTestGuestMode,
GuestModeCreatePermissionContext) {
ASSERT_NO_FATAL_FAILURE(
SmartCardPermissionContextFactory::GetForProfile(*profile()););
}