| // 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 "chrome/browser/device_api/device_service_impl.h" |
| |
| #include <functional> |
| #include <optional> |
| #include <utility> |
| |
| #include "base/check_deref.h" |
| #include "base/check_is_test.h" |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/types/expected_macros.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/app_mode/app_mode_utils.h" |
| #include "chrome/browser/content_settings/host_content_settings_map_factory.h" |
| #include "chrome/browser/device_api/device_attribute_api.h" |
| #include "chrome/browser/permissions/permission_manager_factory.h" |
| #include "chrome/browser/policy/policy_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_constants.h" |
| #include "chrome/browser/web_applications/policy/web_app_policy_constants.h" |
| #include "chrome/browser/web_applications/web_app_filter.h" |
| #include "chrome/browser/web_applications/web_app_provider.h" |
| #include "chrome/browser/web_applications/web_app_registrar.h" |
| #include "chrome/browser/web_applications/web_app_tab_helper.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/content_settings/core/common/pref_names.h" |
| #include "components/permissions/features.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/permission_controller_delegate.h" |
| #include "content/public/browser/permission_descriptor_util.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "third_party/blink/public/common/features_generated.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| #include "ash/constants/ash_features.h" |
| #include "chrome/browser/ash/app_mode/isolated_web_app/kiosk_iwa_data.h" |
| #include "chrome/browser/ash/app_mode/isolated_web_app/kiosk_iwa_manager.h" |
| #include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_data.h" |
| #include "chrome/browser/ash/app_mode/web_app/kiosk_web_app_manager.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/user_manager/user_manager.h" |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| namespace { |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Checks that current user is a web kiosk. |
| bool IsWebKiosk() { |
| return user_manager::UserManager::IsInitialized() && |
| user_manager::UserManager::Get()->IsLoggedInAsKioskWebApp(); |
| } |
| |
| // Checks that current user is an IWA kiosk. |
| bool IsIwaKiosk() { |
| return ash::features::IsIsolatedWebAppKioskEnabled() && |
| user_manager::UserManager::IsInitialized() && |
| user_manager::UserManager::Get()->IsLoggedInAsKioskIWA(); |
| } |
| |
| // Returns an origin of the current kiosk web app. |
| // Should only be called when the current user is a web kiosk. |
| url::Origin GetWebKioskOrigin() { |
| const AccountId& account_id = |
| user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId(); |
| CHECK(ash::KioskWebAppManager::IsInitialized()); |
| const ash::KioskWebAppData* app_data = |
| ash::KioskWebAppManager::Get()->GetAppByAccountId(account_id); |
| return url::Origin::Create(CHECK_DEREF(app_data).install_url()); |
| } |
| |
| // Returns an origin of the current kiosk isolated web app. |
| // Should only be called when the current user is an IWA kiosk. |
| url::Origin GetIwaKioskOrigin() { |
| const AccountId& account_id = |
| user_manager::UserManager::Get()->GetPrimaryUser()->GetAccountId(); |
| const ash::KioskIwaData* iwa_data = |
| CHECK_DEREF(ash::KioskIwaManager::Get()).GetApp(account_id); |
| return CHECK_DEREF(iwa_data).origin(); |
| } |
| |
| std::optional<url::Origin> MaybeGetCurrentKioskOrigin() { |
| if (IsWebKiosk()) { |
| return GetWebKioskOrigin(); |
| } |
| if (IsIwaKiosk()) { |
| return GetIwaKioskOrigin(); |
| } |
| return std::nullopt; |
| } |
| |
| // Check whether the target origin is the same as the main application running |
| // in the Kiosk session. |
| bool IsEqualToKioskOrigin(const url::Origin& origin) { |
| std::optional<url::Origin> current_kiosk_origin = |
| MaybeGetCurrentKioskOrigin(); |
| return current_kiosk_origin.has_value() && |
| (current_kiosk_origin.value() == origin); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| Profile* GetProfile(content::RenderFrameHost& host) { |
| return Profile::FromBrowserContext(host.GetBrowserContext()); |
| } |
| |
| std::optional<std::reference_wrapper<const web_app::WebAppRegistrar>> |
| GetRegistrar(content::RenderFrameHost& host, const url::Origin& origin) { |
| const web_app::WebAppProvider* web_app_provider = |
| web_app::WebAppProvider::GetForWebApps(GetProfile(host)); |
| if (!web_app_provider) { |
| return std::nullopt; |
| } |
| // In this case we will not modify any data so it is safe to access registrar |
| // without lock |
| return web_app_provider->registrar_unsafe(); |
| } |
| |
| std::optional<webapps::AppId> GetAppId( |
| const web_app::WebAppRegistrar& registrar, |
| const url::Origin& origin) { |
| return registrar.FindBestAppWithUrlInScope( |
| origin.GetURL(), web_app::WebAppFilter::InstalledInChrome()); |
| } |
| |
| // Check whether an app with the target origin is in the WebAppRegistrar and is |
| // an IWA. |
| bool IsForceInstalledIwaOrigin(content::RenderFrameHost& host, |
| const url::Origin& origin) { |
| ASSIGN_OR_RETURN(const web_app::WebAppRegistrar& registrar, |
| GetRegistrar(host, origin), [] { return false; }); |
| ASSIGN_OR_RETURN(webapps::AppId app_id, GetAppId(registrar, origin), |
| [] { return false; }); |
| return registrar.IsInstalledByPolicy(app_id) && registrar.IsIsolated(app_id); |
| } |
| |
| // Check whether an app with the target origin is in the WebAppRegistrar. |
| bool IsForceInstalledOrigin(content::RenderFrameHost& host, |
| const url::Origin& origin) { |
| ASSIGN_OR_RETURN(const web_app::WebAppRegistrar& registrar, |
| GetRegistrar(host, origin), [] { return false; }); |
| ASSIGN_OR_RETURN(webapps::AppId app_id, GetAppId(registrar, origin), |
| [] { return false; }); |
| return registrar.IsInstalledByPolicy(app_id); |
| } |
| |
| bool IsAffiliatedUser() { |
| #if BUILDFLAG(IS_CHROMEOS) |
| const user_manager::User* user = |
| user_manager::UserManager::Get()->GetPrimaryUser(); |
| return (user != nullptr) && user->IsAffiliated(); |
| #else |
| return false; |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| bool IsPermissionsPolicyFeatureEnabled() { |
| return base::FeatureList::IsEnabled( |
| blink::features::kDeviceAttributesPermissionPolicy); |
| } |
| |
| bool IsTrustedContext(content::RenderFrameHost& host, |
| const url::Origin& origin) { |
| // Do not create the service for the incognito mode. |
| if (GetProfile(host)->IsIncognitoProfile()) { |
| return false; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| if (IsRunningInAppMode()) { |
| if (base::FeatureList::IsEnabled( |
| permissions::features:: |
| kAllowMultipleOriginsForWebKioskPermissions)) { |
| return IsEqualToKioskOrigin(origin) || |
| IsWebKioskOriginAllowed(GetProfile(host)->GetPrefs(), |
| origin.GetURL()); |
| } |
| |
| return IsEqualToKioskOrigin(origin); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| return IsPermissionsPolicyFeatureEnabled() |
| ? IsForceInstalledIwaOrigin(host, origin) |
| : IsForceInstalledOrigin(host, origin); |
| } |
| |
| bool IsAllowedByPermissionsPolicy(content::RenderFrameHost& host) { |
| return host.IsFeatureEnabled( |
| network::mojom::PermissionsPolicyFeature::kDeviceAttributes); |
| } |
| |
| bool IsAllowedByAdminPolicy(content::RenderFrameHost& host, |
| const url::Origin& origin) { |
| #if BUILDFLAG(IS_CHROMEOS) |
| return policy::IsOriginInAllowlist( |
| origin.GetURL(), GetProfile(host)->GetPrefs(), |
| prefs::kManagedDeviceAttributesAllowedForOrigins); |
| #else |
| return false; |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| bool IsAllowedByContentSettings(content::RenderFrameHost& host, |
| const url::Origin& origin) { |
| switch (HostContentSettingsMapFactory::GetForProfile(GetProfile(host)) |
| ->GetContentSetting(origin.GetURL(), origin.GetURL(), |
| ContentSettingsType::DEVICE_ATTRIBUTES)) { |
| case CONTENT_SETTING_ALLOW: |
| return true; |
| case CONTENT_SETTING_BLOCK: |
| return false; |
| default: |
| NOTREACHED(); |
| } |
| } |
| } // namespace |
| |
| DeviceServiceImpl::DeviceServiceImpl( |
| content::RenderFrameHost& host, |
| mojo::PendingReceiver<blink::mojom::DeviceAPIService> receiver, |
| std::unique_ptr<DeviceAttributeApi> device_attribute_api) |
| : DocumentService(host, std::move(receiver)), |
| device_attribute_api_(std::move(device_attribute_api)) { |
| Profile* const profile = GetProfile(host); |
| pref_change_registrar_.Init(profile->GetPrefs()); |
| pref_change_registrar_.Add( |
| prefs::kWebAppInstallForceList, |
| base::BindRepeating(&DeviceServiceImpl::OnDisposingIfNeeded, |
| base::Unretained(this))); |
| pref_change_registrar_.Add( |
| prefs::kIsolatedWebAppInstallForceList, |
| base::BindRepeating(&DeviceServiceImpl::OnDisposingIfNeeded, |
| base::Unretained(this))); |
| #if BUILDFLAG(IS_CHROMEOS) |
| pref_change_registrar_.Add( |
| prefs::kKioskBrowserPermissionsAllowedForOrigins, |
| base::BindRepeating(&DeviceServiceImpl::OnDisposingIfNeeded, |
| base::Unretained(this))); |
| if (!IsPermissionsPolicyFeatureEnabled()) { |
| pref_change_registrar_.Add( |
| prefs::kManagedDeviceAttributesAllowedForOrigins, |
| base::BindRepeating(&DeviceServiceImpl::OnDisposingIfNeeded, |
| base::Unretained(this))); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| content_settings_observation_.Observe( |
| HostContentSettingsMapFactory::GetForProfile(profile)); |
| auto& provider = CHECK_DEREF(web_app::WebAppProvider::GetForWebApps(profile)); |
| install_manager_observation_.Observe(&provider.install_manager()); |
| } |
| |
| DeviceServiceImpl::~DeviceServiceImpl() = default; |
| |
| // static |
| void DeviceServiceImpl::Create( |
| content::RenderFrameHost* host, |
| mojo::PendingReceiver<blink::mojom::DeviceAPIService> receiver, |
| std::unique_ptr<DeviceAttributeApi> device_attribute_api) { |
| CHECK(host); |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| if (host->GetParentOrOuterDocumentOrEmbedder()) { |
| mojo::ReportBadMessage( |
| "Device Attributes are allowed only in top level frames."); |
| return; |
| } |
| if (!IsTrustedContext(*host, host->GetLastCommittedOrigin())) { |
| // Not sending bad message here since the API is always exposed to the end |
| // user. |
| return; |
| } |
| if (IsPermissionsPolicyFeatureEnabled() && |
| !IsAllowedByPermissionsPolicy(*host)) { |
| mojo::ReportBadMessage( |
| "Permissions policy blocks access to Device Attributes."); |
| return; |
| } |
| // The object is bound to the lifetime of |host| and the mojo |
| // connection. See DocumentService for details. |
| new DeviceServiceImpl(*host, std::move(receiver), |
| std::move(device_attribute_api)); |
| } |
| |
| // static |
| void DeviceServiceImpl::Create( |
| content::RenderFrameHost* host, |
| mojo::PendingReceiver<blink::mojom::DeviceAPIService> receiver) { |
| Create(host, std::move(receiver), std::make_unique<DeviceAttributeApiImpl>()); |
| } |
| |
| // static |
| void DeviceServiceImpl::CreateForTest( |
| content::RenderFrameHost* host, |
| mojo::PendingReceiver<blink::mojom::DeviceAPIService> receiver, |
| std::unique_ptr<DeviceAttributeApi> device_attribute_api) { |
| CHECK_IS_TEST(); |
| Create(host, std::move(receiver), std::move(device_attribute_api)); |
| } |
| |
| void DeviceServiceImpl::OnWebAppSourceRemoved(const webapps::AppId& app_id) { |
| OnDisposingIfNeeded(); |
| } |
| |
| void DeviceServiceImpl::OnWebAppUninstalled( |
| const webapps::AppId& app_id, |
| webapps::WebappUninstallSource uninstall_source) { |
| OnDisposingIfNeeded(); |
| } |
| |
| void DeviceServiceImpl::OnWebAppInstallManagerDestroyed() { |
| install_manager_observation_.Reset(); |
| } |
| |
| void DeviceServiceImpl::OnContentSettingChanged( |
| const ContentSettingsPattern& primary_pattern, |
| const ContentSettingsPattern& secondary_pattern, |
| ContentSettingsTypeSet content_type_set) { |
| if (!content_type_set.Contains(ContentSettingsType::DEVICE_ATTRIBUTES)) { |
| return; |
| } |
| OnDisposingIfNeeded(); |
| } |
| |
| void DeviceServiceImpl::OnDisposingIfNeeded() { |
| // DeviceServiceImpl is allocated on the heap, thus it is safe to remove it |
| // like this. |
| if (!IsTrustedContext(render_frame_host(), origin())) { |
| ResetAndDeleteThis(); |
| } |
| } |
| |
| void DeviceServiceImpl::GetDirectoryId(GetDirectoryIdCallback callback) { |
| GetDeviceAttribute(&DeviceAttributeApi::GetDirectoryId, std::move(callback)); |
| } |
| |
| void DeviceServiceImpl::GetHostname(GetHostnameCallback callback) { |
| GetDeviceAttribute(&DeviceAttributeApi::GetHostname, std::move(callback)); |
| } |
| |
| void DeviceServiceImpl::GetSerialNumber(GetSerialNumberCallback callback) { |
| GetDeviceAttribute(&DeviceAttributeApi::GetSerialNumber, std::move(callback)); |
| } |
| |
| void DeviceServiceImpl::GetAnnotatedAssetId( |
| GetAnnotatedAssetIdCallback callback) { |
| GetDeviceAttribute(&DeviceAttributeApi::GetAnnotatedAssetId, |
| std::move(callback)); |
| } |
| |
| void DeviceServiceImpl::GetAnnotatedLocation( |
| GetAnnotatedLocationCallback callback) { |
| GetDeviceAttribute(&DeviceAttributeApi::GetAnnotatedLocation, |
| std::move(callback)); |
| } |
| |
| void DeviceServiceImpl::GetDeviceAttribute( |
| void (DeviceAttributeApi::*method)(DeviceAttributeCallback callback), |
| DeviceAttributeCallback callback) { |
| if (!IsAffiliatedUser()) { |
| device_attribute_api_->ReportNotAffiliatedError(std::move(callback)); |
| return; |
| } |
| |
| if (IsPermissionsPolicyFeatureEnabled()) { |
| if (!IsAllowedByContentSettings(render_frame_host(), origin())) { |
| device_attribute_api_->ReportNotAllowedError(std::move(callback)); |
| return; |
| } |
| } else { |
| if (!IsAllowedByAdminPolicy(render_frame_host(), origin())) { |
| device_attribute_api_->ReportNotAllowedError(std::move(callback)); |
| return; |
| } |
| } |
| |
| (device_attribute_api_.get()->*method)(std::move(callback)); |
| } |