| // Copyright 2024 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/ash/app_mode/test/kiosk_mixin.h" |
| |
| #include <algorithm> |
| #include <functional> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "ash/constants/ash_switches.h" |
| #include "base/check_op.h" |
| #include "base/command_line.h" |
| #include "base/containers/flat_set.h" |
| #include "base/files/file_path.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "chrome/browser/app_mode/test/fake_origin_test_server_mixin.h" |
| #include "chrome/browser/ash/app_mode/fake_cws.h" |
| #include "chrome/browser/ash/app_mode/fake_cws_mixin.h" |
| #include "chrome/browser/ash/app_mode/kiosk_test_helper.h" |
| #include "chrome/browser/ash/login/app_mode/network_ui_controller.h" |
| #include "chrome/browser/ash/login/test/device_state_mixin.h" |
| #include "chrome/browser/ash/login/test/scoped_policy_update.h" |
| #include "chrome/test/base/mixin_based_in_process_browser_test.h" |
| #include "components/policy/core/common/cloud/test/policy_builder.h" |
| #include "components/policy/proto/chrome_device_policy.pb.h" |
| #include "components/web_package/signed_web_bundles/signed_web_bundle_id.h" |
| #include "components/webapps/isolated_web_apps/iwa_key_distribution_info_provider.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/abseil-cpp/absl/functional/overload.h" |
| #include "url/gurl.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| using enterprise_management::DeviceLocalAccountInfoProto; |
| using enterprise_management::DeviceLocalAccountsProto; |
| |
| constexpr std::string_view kDefaultWebAppOrigin = "https://kioskmixinapp.com"; |
| |
| constexpr base::FilePath::StringViewType kDefaultWebAppPath = |
| FILE_PATH_LITERAL("chrome/test/data"); |
| |
| void AppendSwitchesToDisplayLoginScreen(base::CommandLine* command_line) { |
| command_line->AppendSwitch(switches::kLoginManager); |
| command_line->AppendSwitch(switches::kForceLoginManagerInTests); |
| } |
| |
| std::string_view GetAccountId(const KioskMixin::Option& option) { |
| return std::visit( |
| absl::Overload{ |
| [](const KioskMixin::DefaultServerWebAppOption& option) { |
| return std::string_view(option.account_id); |
| }, |
| [](const KioskMixin::WebAppOption& option) { |
| return std::string_view(option.account_id); |
| }, |
| [](const KioskMixin::CwsChromeAppOption& option) { |
| return std::string_view(option.account_id); |
| }, |
| [](const KioskMixin::SelfHostedChromeAppOption& option) { |
| return std::string_view(option.account_id); |
| }, |
| [](const KioskMixin::IsolatedWebAppOption& option) { |
| return std::string_view(option.account_id); |
| }, |
| }, |
| option); |
| } |
| |
| // Returns the Web app URL of the given `option`, or the empty `GURL` if it's |
| // not a Web app option. |
| GURL GetWebAppUrl(const KioskMixin::Option& option) { |
| return std::visit( |
| absl::Overload{ |
| [](const KioskMixin::DefaultServerWebAppOption& option) { |
| return GURL(kDefaultWebAppOrigin).Resolve(option.url_path); |
| }, |
| [](const KioskMixin::WebAppOption& option) { return option.url; }, |
| [](const KioskMixin::CwsChromeAppOption& option) { return GURL(); }, |
| [](const KioskMixin::SelfHostedChromeAppOption& option) { |
| return GURL(); |
| }, |
| [](const KioskMixin::IsolatedWebAppOption& option) { return GURL(); }, |
| }, |
| option); |
| } |
| |
| // Returns the Chrome app ID of the given `option`, or the empty string if it's |
| // not a Chrome app option. |
| std::string_view GetChromeAppId(const KioskMixin::Option& option) { |
| return std::visit( |
| absl::Overload{ |
| [](const KioskMixin::DefaultServerWebAppOption& option) { |
| return std::string_view(); |
| }, |
| [](const KioskMixin::WebAppOption& option) { |
| return std::string_view(); |
| }, |
| [](const KioskMixin::CwsChromeAppOption& option) { |
| return std::string_view(option.app_id); |
| }, |
| [](const KioskMixin::SelfHostedChromeAppOption& option) { |
| return std::string_view(option.app_id); |
| }, |
| [](const KioskMixin::IsolatedWebAppOption& option) { |
| return std::string_view(); |
| }, |
| }, |
| option); |
| } |
| |
| // Runs multiple checks on `config` to avoid common errors. |
| void CheckIsValid(const KioskMixin::Config& config) { |
| // There must be at least one Kiosk app. |
| CHECK_NE(0ul, config.options.size()); |
| |
| // No two apps can have the same account ID. |
| auto configured_accounts = base::MakeFlatSet<std::string_view>( |
| config.options, std::less(), GetAccountId); |
| CHECK_EQ(configured_accounts.size(), config.options.size()); |
| |
| // If there is an auto launch app, there must also be a config option for it. |
| if (config.auto_launch_account_id.has_value()) { |
| CHECK_NE(0l, |
| std::ranges::count(config.options, |
| config.auto_launch_account_id.value().value(), |
| GetAccountId)); |
| } |
| |
| // No two Web apps can have the same URL. |
| for (auto it = config.options.begin(); it != config.options.end(); it++) { |
| if (GURL url = GetWebAppUrl(*it); url.is_valid()) { |
| CHECK_EQ(1, std::ranges::count(config.options, url, GetWebAppUrl)); |
| } |
| } |
| |
| // No two Chrome apps can have the same app ID. |
| for (auto it = config.options.begin(); it != config.options.end(); it++) { |
| if (std::string_view app_id = GetChromeAppId(*it); !app_id.empty()) { |
| CHECK_EQ(1, std::ranges::count(config.options, app_id, GetChromeAppId)); |
| } |
| } |
| } |
| |
| // Configures a Kiosk Chrome App in device policies with the given `app_id` |
| // and `account_id`. |
| void ConfigureCwsChromeApp(ScopedDevicePolicyUpdate& update, |
| std::string_view app_id, |
| std::string_view account_id) { |
| DeviceLocalAccountInfoProto* account = |
| update.policy_payload()->mutable_device_local_accounts()->add_account(); |
| |
| account->set_account_id(std::string(account_id)); |
| account->set_type(DeviceLocalAccountInfoProto::ACCOUNT_TYPE_KIOSK_APP); |
| account->mutable_kiosk_app()->set_app_id(std::string(app_id)); |
| } |
| |
| void ConfigureSelfHostedChromeApp(ScopedDevicePolicyUpdate& update, |
| std::string_view app_id, |
| std::string_view account_id, |
| const GURL& update_url) { |
| DeviceLocalAccountInfoProto* account = |
| update.policy_payload()->mutable_device_local_accounts()->add_account(); |
| |
| account->set_account_id(std::string(account_id)); |
| account->set_type(DeviceLocalAccountInfoProto::ACCOUNT_TYPE_KIOSK_APP); |
| account->mutable_kiosk_app()->set_update_url(update_url.spec()); |
| account->mutable_kiosk_app()->set_app_id(std::string(app_id)); |
| } |
| |
| // Configures a Kiosk web app in device policies with the given `url` and |
| // `account_id`. |
| void ConfigureWebApp(ScopedDevicePolicyUpdate& update, |
| const GURL& url, |
| std::string_view account_id) { |
| DeviceLocalAccountInfoProto* account = |
| update.policy_payload()->mutable_device_local_accounts()->add_account(); |
| |
| account->set_account_id(std::string(account_id)); |
| account->set_type(DeviceLocalAccountInfoProto::ACCOUNT_TYPE_WEB_KIOSK_APP); |
| account->mutable_web_kiosk_app()->set_url(url.spec()); |
| } |
| |
| // Configures a Kiosk isolated web app and related device policies. |
| void ConfigureIsolatedWebApp(ScopedDevicePolicyUpdate& update, |
| const KioskMixin::IsolatedWebAppOption& option) { |
| web_app::IwaKeyDistributionInfoProvider::GetInstance() |
| .SkipManagedAllowlistChecksForTesting(option.skip_iwa_allowlist_checks); |
| |
| DeviceLocalAccountInfoProto* account = |
| update.policy_payload()->mutable_device_local_accounts()->add_account(); |
| |
| account->set_account_id(std::string(option.account_id)); |
| account->set_type(DeviceLocalAccountInfoProto::ACCOUNT_TYPE_KIOSK_IWA); |
| account->mutable_isolated_kiosk_app()->set_web_bundle_id( |
| option.web_bundle_id.id()); |
| account->mutable_isolated_kiosk_app()->set_update_manifest_url( |
| option.update_manifest_url.spec()); |
| account->mutable_isolated_kiosk_app()->set_update_channel( |
| option.update_channel); |
| account->mutable_isolated_kiosk_app()->set_pinned_version( |
| option.pinned_version); |
| account->mutable_isolated_kiosk_app()->set_allow_downgrades( |
| option.allow_downgrades); |
| } |
| |
| // Configures the Kiosk account given by `account_id` as the auto launch |
| // account. |
| void ConfigureAutoLaunchAccountId(ScopedDevicePolicyUpdate& update, |
| std::string_view account_id) { |
| update.policy_payload()->mutable_device_local_accounts()->set_auto_login_id( |
| std::string(account_id)); |
| } |
| |
| // Configures the default user policies applied by DM server for Kiosk Web apps |
| // and IWAs into `update`. |
| void ConfigureDefaultWebAppUserPolicies(ScopedUserPolicyUpdate& update) { |
| update.policy_payload() |
| ->mutable_extensioninstallblocklist() |
| ->mutable_value() |
| ->add_entries("*"); |
| } |
| |
| bool HasChromeApps(KioskMixin::Config config) { |
| return std::ranges::any_of(config.options, [](const auto& option) { |
| return std::holds_alternative<KioskMixin::CwsChromeAppOption>(option) || |
| std::holds_alternative<KioskMixin::SelfHostedChromeAppOption>( |
| option); |
| }); |
| } |
| |
| } // namespace |
| |
| KioskMixin::KioskMixin(InProcessBrowserTestMixinHost* host, |
| std::optional<Config> cached_configuration) |
| : InProcessBrowserTestMixin(host), |
| cached_configuration_(std::move(cached_configuration)), |
| skip_splash_screen_override_(KioskTestHelper::SkipSplashScreenWait()), |
| network_wait_override_( |
| NetworkUiController::SetNetworkWaitTimeoutForTesting( |
| base::TimeDelta())), |
| web_server_(host, GURL(kDefaultWebAppOrigin), kDefaultWebAppPath), |
| fake_cws_mixin_(host, FakeCwsMixin::kPublic), |
| device_state_( |
| host, |
| ash::DeviceStateMixin::State::OOBE_COMPLETED_CLOUD_ENROLLED) { |
| // Chrome apps default to disabled in Kiosk from M138. Re-enable Chrome apps |
| // in tests that need it. Tests can initialize their own `ScopedFeatureList` |
| // separately to override this setting if needed. |
| if (cached_configuration_.has_value() && |
| HasChromeApps(cached_configuration_.value())) { |
| scoped_features_.InitFromCommandLine("AllowChromeAppsInKioskSessions", ""); |
| } |
| } |
| |
| KioskMixin::KioskMixin(InProcessBrowserTestMixinHost* host, |
| Config cached_configuration) |
| : KioskMixin(host, std::make_optional(std::move(cached_configuration))) {} |
| |
| KioskMixin::KioskMixin(InProcessBrowserTestMixinHost* host) |
| : KioskMixin(host, /*cached_configuration=*/{}) {} |
| |
| KioskMixin::~KioskMixin() = default; |
| |
| void KioskMixin::SetUpCommandLine(base::CommandLine* command_line) { |
| AppendSwitchesToDisplayLoginScreen(command_line); |
| } |
| |
| void KioskMixin::SetUpInProcessBrowserTestFixture() { |
| if (cached_configuration_.has_value()) { |
| Configure(*device_state_.RequestDevicePolicyUpdate().get(), |
| cached_configuration_.value()); |
| } |
| } |
| |
| void KioskMixin::Configure(ScopedDevicePolicyUpdate& device_policy_update, |
| const Config& config) { |
| auto user_policy_update_callback = |
| base::BindLambdaForTesting([this](std::string_view account_id) { |
| return device_state_.RequestDeviceLocalAccountPolicyUpdate( |
| std::string(account_id)); |
| }); |
| Configure(device_policy_update, user_policy_update_callback, config); |
| } |
| |
| void KioskMixin::Configure(ScopedDevicePolicyUpdate& device_policy_update, |
| UserPolicyUpdateCallback user_policy_update_callback, |
| const Config& config) { |
| CheckIsValid(config); |
| |
| for (const auto& option : config.options) { |
| auto account_id = GetAccountId(option); |
| auto user_policy_update = user_policy_update_callback.Run(account_id); |
| std::visit( |
| absl::Overload{ |
| [this, &device_policy_update, |
| &user_policy_update](const DefaultServerWebAppOption& option) { |
| ConfigureWebApp(device_policy_update, |
| web_server_.GetUrl(option.url_path), |
| option.account_id); |
| ConfigureDefaultWebAppUserPolicies(*user_policy_update); |
| }, |
| [&device_policy_update, |
| &user_policy_update](const WebAppOption& option) { |
| ConfigureWebApp(device_policy_update, option.url, |
| option.account_id); |
| ConfigureDefaultWebAppUserPolicies(*user_policy_update); |
| }, |
| [this, &device_policy_update](const CwsChromeAppOption& option) { |
| fake_cws().SetUpdateCrx(option.app_id, option.crx_filename, |
| option.crx_version); |
| ConfigureCwsChromeApp(device_policy_update, option.app_id, |
| option.account_id); |
| }, |
| [&device_policy_update](const SelfHostedChromeAppOption& option) { |
| ConfigureSelfHostedChromeApp(device_policy_update, option.app_id, |
| option.account_id, |
| option.update_url); |
| }, |
| [&device_policy_update, |
| &user_policy_update](const IsolatedWebAppOption& option) { |
| ConfigureIsolatedWebApp(device_policy_update, option); |
| ConfigureDefaultWebAppUserPolicies(*user_policy_update); |
| }, |
| }, |
| option); |
| } |
| |
| if (config.auto_launch_account_id.has_value()) { |
| ConfigureAutoLaunchAccountId(device_policy_update, |
| config.auto_launch_account_id->value()); |
| } |
| } |
| |
| GURL KioskMixin::GetDefaultServerUrl(std::string_view url_suffix) const { |
| return web_server_.GetUrl(url_suffix); |
| } |
| |
| // static |
| std::vector<KioskMixin::Config> KioskMixin::ConfigsToAutoLaunchEachAppType() { |
| // TODO(crbug.com/379633748): Add IWA. |
| return { |
| Config{/*name=*/"WebApp", |
| AutoLaunchAccount{SimpleWebAppOption().account_id}, |
| {SimpleWebAppOption()}}, |
| Config{/*name=*/"ChromeApp", |
| AutoLaunchAccount{SimpleChromeAppOption().account_id}, |
| {SimpleChromeAppOption()}}, |
| }; |
| } |
| |
| // static |
| KioskMixin::DefaultServerWebAppOption KioskMixin::SimpleWebAppOption() { |
| // Serves //chrome/test/data/title3.html. |
| return DefaultServerWebAppOption{/*account_id=*/"simple-web-app@localhost", |
| /*url_path=*/"/title3.html"}; |
| } |
| |
| // static |
| KioskMixin::CwsChromeAppOption KioskMixin::SimpleChromeAppOption() { |
| // Configures the Chrome app in: |
| // //chrome/test/data/chromeos/app_mode/apps_and_extensions/kiosk_test_app. |
| static constexpr char kChromeAppId[] = "ggaeimfdpnmlhdhpcikgoblffmkckdmn"; |
| |
| return CwsChromeAppOption{ |
| /*account_id=*/"simple-chrome-app@localhost", |
| /*app_id=*/kChromeAppId, |
| /*crx_filename=*/base::StrCat({kChromeAppId, ".crx"}), |
| /*crx_version=*/"1.0.0"}; |
| } |
| |
| // static |
| std::string KioskMixin::ConfigName(const testing::TestParamInfo<Config>& info) { |
| return info.param.name.value_or(base::StringPrintf("%zu", info.index)); |
| } |
| |
| KioskMixin::DefaultServerWebAppOption::DefaultServerWebAppOption( |
| std::string_view account_id, |
| std::string_view url_path) |
| : account_id(std::string(account_id)), url_path(std::string(url_path)) {} |
| |
| KioskMixin::DefaultServerWebAppOption::DefaultServerWebAppOption( |
| const KioskMixin::DefaultServerWebAppOption&) = default; |
| KioskMixin::DefaultServerWebAppOption::DefaultServerWebAppOption( |
| KioskMixin::DefaultServerWebAppOption&&) = default; |
| KioskMixin::DefaultServerWebAppOption& |
| KioskMixin::DefaultServerWebAppOption::operator=( |
| const KioskMixin::DefaultServerWebAppOption&) = default; |
| KioskMixin::DefaultServerWebAppOption& |
| KioskMixin::DefaultServerWebAppOption::operator=( |
| KioskMixin::DefaultServerWebAppOption&&) = default; |
| KioskMixin::DefaultServerWebAppOption::~DefaultServerWebAppOption() = default; |
| |
| KioskMixin::WebAppOption::WebAppOption(std::string_view account_id, GURL url) |
| : account_id(std::string(account_id)), url(std::move(url)) {} |
| |
| KioskMixin::WebAppOption::WebAppOption(const KioskMixin::WebAppOption&) = |
| default; |
| KioskMixin::WebAppOption::WebAppOption(KioskMixin::WebAppOption&&) = default; |
| KioskMixin::WebAppOption& KioskMixin::WebAppOption::operator=( |
| const KioskMixin::WebAppOption&) = default; |
| KioskMixin::WebAppOption& KioskMixin::WebAppOption::operator=( |
| KioskMixin::WebAppOption&&) = default; |
| KioskMixin::WebAppOption::~WebAppOption() = default; |
| |
| KioskMixin::CwsChromeAppOption::CwsChromeAppOption( |
| std::string_view account_id, |
| std::string_view app_id, |
| std::string_view crx_filename, |
| std::string_view crx_version) |
| : account_id(std::string(account_id)), |
| app_id(std::string(app_id)), |
| crx_filename(std::string(crx_filename)), |
| crx_version(std::string(crx_version)) {} |
| |
| KioskMixin::CwsChromeAppOption::CwsChromeAppOption( |
| const KioskMixin::CwsChromeAppOption&) = default; |
| KioskMixin::CwsChromeAppOption::CwsChromeAppOption( |
| KioskMixin::CwsChromeAppOption&&) = default; |
| KioskMixin::CwsChromeAppOption& KioskMixin::CwsChromeAppOption::operator=( |
| const KioskMixin::CwsChromeAppOption&) = default; |
| KioskMixin::CwsChromeAppOption& KioskMixin::CwsChromeAppOption::operator=( |
| KioskMixin::CwsChromeAppOption&&) = default; |
| KioskMixin::CwsChromeAppOption::~CwsChromeAppOption() = default; |
| |
| KioskMixin::SelfHostedChromeAppOption::SelfHostedChromeAppOption( |
| std::string_view account_id, |
| std::string_view app_id, |
| const GURL& update_url) |
| : account_id(std::string(account_id)), |
| app_id(std::string(app_id)), |
| update_url(update_url) {} |
| |
| KioskMixin::SelfHostedChromeAppOption::SelfHostedChromeAppOption( |
| const KioskMixin::SelfHostedChromeAppOption&) = default; |
| KioskMixin::SelfHostedChromeAppOption::SelfHostedChromeAppOption( |
| KioskMixin::SelfHostedChromeAppOption&&) = default; |
| KioskMixin::SelfHostedChromeAppOption& |
| KioskMixin::SelfHostedChromeAppOption::operator=( |
| const KioskMixin::SelfHostedChromeAppOption&) = default; |
| KioskMixin::SelfHostedChromeAppOption& |
| KioskMixin::SelfHostedChromeAppOption::operator=( |
| KioskMixin::SelfHostedChromeAppOption&&) = default; |
| KioskMixin::SelfHostedChromeAppOption::~SelfHostedChromeAppOption() = default; |
| |
| KioskMixin::IsolatedWebAppOption::IsolatedWebAppOption( |
| std::string_view account_id, |
| const web_package::SignedWebBundleId& web_bundle_id, |
| GURL update_manifest_url, |
| std::string update_channel, |
| std::string pinned_version, |
| bool allow_downgrades, |
| bool skip_iwa_allowlist_checks) |
| : account_id(std::string(account_id)), |
| web_bundle_id(web_bundle_id), |
| update_manifest_url(std::move(update_manifest_url)), |
| update_channel(std::move(update_channel)), |
| pinned_version(std::move(pinned_version)), |
| allow_downgrades(allow_downgrades), |
| skip_iwa_allowlist_checks(skip_iwa_allowlist_checks) {} |
| |
| KioskMixin::IsolatedWebAppOption::IsolatedWebAppOption( |
| const KioskMixin::IsolatedWebAppOption&) = default; |
| KioskMixin::IsolatedWebAppOption::IsolatedWebAppOption( |
| KioskMixin::IsolatedWebAppOption&&) = default; |
| KioskMixin::IsolatedWebAppOption& KioskMixin::IsolatedWebAppOption::operator=( |
| const KioskMixin::IsolatedWebAppOption&) = default; |
| KioskMixin::IsolatedWebAppOption& KioskMixin::IsolatedWebAppOption::operator=( |
| KioskMixin::IsolatedWebAppOption&&) = default; |
| KioskMixin::IsolatedWebAppOption::~IsolatedWebAppOption() = default; |
| |
| KioskMixin::Config::Config( |
| std::optional<std::string> name, |
| std::optional<AutoLaunchAccount> auto_launch_account_id, |
| std::vector<Option> options) |
| : name(std::move(name)), |
| auto_launch_account_id(std::move(auto_launch_account_id)), |
| options(std::move(options)) {} |
| |
| KioskMixin::Config::Config(const KioskMixin::Config&) = default; |
| KioskMixin::Config::Config(KioskMixin::Config&&) = default; |
| KioskMixin::Config& KioskMixin::Config::Config::operator=( |
| const KioskMixin::Config&) = default; |
| KioskMixin::Config& KioskMixin::Config::Config::operator=( |
| KioskMixin::Config&&) = default; |
| KioskMixin::Config::~Config() = default; |
| |
| } // namespace ash |