| // 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/floating_sso/floating_sso_service.h" |
| |
| #include <memory> |
| |
| #include "ash/constants/ash_features.h" |
| #include "ash/constants/ash_pref_names.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/values.h" |
| #include "chrome/browser/ash/floating_sso/cookie_sync_conversions.h" |
| #include "chrome/browser/ash/floating_sso/floating_sso_sync_bridge.h" |
| #include "chrome/browser/ash/floating_workspace/floating_workspace_util.h" |
| #include "chrome/common/pref_names.h" |
| #include "chromeos/constants/pref_names.h" |
| #include "components/google/core/common/google_util.h" |
| #include "components/prefs/pref_change_registrar.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/sync/base/pref_names.h" |
| #include "components/url_matcher/url_util.h" |
| #include "net/cookies/cookie_change_dispatcher.h" |
| #include "net/cookies/cookie_util.h" |
| |
| namespace ash::floating_sso { |
| |
| namespace { |
| |
| bool IsGoogleUrl(const GURL& url) { |
| return google_util::IsGoogleDomainUrl( |
| url, google_util::ALLOW_SUBDOMAIN, |
| google_util::ALLOW_NON_STANDARD_PORTS) || |
| google_util::IsYoutubeDomainUrl(url, google_util::ALLOW_SUBDOMAIN, |
| google_util::ALLOW_NON_STANDARD_PORTS); |
| } |
| |
| } // namespace |
| |
| FloatingSsoService::FloatingSsoService( |
| PrefService* prefs, |
| std::unique_ptr<FloatingSsoSyncBridge> bridge, |
| CookieManagerGetter cookie_manager_getter) |
| : prefs_(prefs), |
| cookie_manager_getter_(cookie_manager_getter), |
| bridge_(std::move(bridge)), |
| pref_change_registrar_(std::make_unique<PrefChangeRegistrar>()) { |
| pref_change_registrar_->Init(prefs_); |
| RegisterPolicyListeners(); |
| UpdateUrlMatchers(); |
| StartOrStop(); |
| } |
| |
| FloatingSsoService::~FloatingSsoService() = default; |
| |
| void FloatingSsoService::Shutdown() { |
| pref_change_registrar_.reset(); |
| prefs_ = nullptr; |
| } |
| |
| void FloatingSsoService::RegisterPolicyListeners() { |
| pref_change_registrar_->Add( |
| chromeos::prefs::kFloatingSsoEnabled, |
| base::BindRepeating(&FloatingSsoService::StartOrStop, |
| base::Unretained(this))); |
| pref_change_registrar_->Add( |
| syncer::prefs::internal::kSyncKeepEverythingSynced, |
| base::BindRepeating(&FloatingSsoService::StartOrStop, |
| base::Unretained(this))); |
| pref_change_registrar_->Add( |
| syncer::prefs::internal::kSyncCookies, |
| base::BindRepeating(&FloatingSsoService::StartOrStop, |
| base::Unretained(this))); |
| pref_change_registrar_->Add( |
| syncer::prefs::internal::kSyncManaged, |
| base::BindRepeating(&FloatingSsoService::StartOrStop, |
| base::Unretained(this))); |
| // Policy updates will only affect future updates of cookies, this means that |
| // cookies that already exist are not checked again to see if some of them are |
| // no longer blocklisted. |
| pref_change_registrar_->Add( |
| ::prefs::kFloatingSsoDomainBlocklist, |
| base::BindRepeating(&FloatingSsoService::UpdateUrlMatchers, |
| base::Unretained(this))); |
| pref_change_registrar_->Add( |
| ::prefs::kFloatingSsoDomainBlocklistExceptions, |
| base::BindRepeating(&FloatingSsoService::UpdateUrlMatchers, |
| base::Unretained(this))); |
| } |
| |
| void FloatingSsoService::UpdateUrlMatchers() { |
| // Reset URL matchers every time the policies change. |
| block_url_matcher_ = std::make_unique<url_matcher::URLMatcher>(); |
| except_url_matcher_ = std::make_unique<url_matcher::URLMatcher>(); |
| |
| const base::Value::List& blocklist = |
| prefs_->GetList(::prefs::kFloatingSsoDomainBlocklist); |
| const base::Value::List& blocklist_exceptions = |
| prefs_->GetList(::prefs::kFloatingSsoDomainBlocklistExceptions); |
| |
| if (!blocklist.empty()) { |
| base::MatcherStringPattern::ID block_id = 0; |
| url_matcher::util::AddFiltersWithLimit( |
| block_url_matcher_.get(), /*allow=*/false, &block_id, blocklist); |
| } |
| |
| if (!blocklist_exceptions.empty()) { |
| base::MatcherStringPattern::ID except_id = 0; |
| url_matcher::util::AddFiltersWithLimit(except_url_matcher_.get(), |
| /*allow=*/true, &except_id, |
| blocklist_exceptions); |
| } |
| } |
| |
| void FloatingSsoService::StartOrStop() { |
| if (IsFloatingSsoEnabled()) { |
| if (!scoped_observation_.IsObserving()) { |
| scoped_observation_.Observe(bridge_.get()); |
| } |
| MaybeStartListening(); |
| } else { |
| scoped_observation_.Reset(); |
| StopListening(); |
| } |
| } |
| |
| bool FloatingSsoService::IsFloatingSsoEnabled() { |
| // Check FloatingSsoEnabled policy. |
| if (!prefs_->GetBoolean(chromeos::prefs::kFloatingSsoEnabled)) { |
| return false; |
| } |
| // Check SyncDisabled policy (it maps to kSyncManaged pref). |
| if (prefs_->GetBoolean(syncer::prefs::internal::kSyncManaged)) { |
| return false; |
| } |
| // Check that user either syncs everything or has selected cookies as one of |
| // synced types. |
| if (!prefs_->GetBoolean(syncer::prefs::internal::kSyncKeepEverythingSynced)) { |
| return prefs_->GetBoolean(syncer::prefs::internal::kSyncCookies); |
| } |
| return true; |
| } |
| |
| void FloatingSsoService::RunWhenCookiesAreReady(base::OnceClosure callback) { |
| if (changes_in_progress_count_ == 0) { |
| std::move(callback).Run(); |
| } else { |
| on_no_changes_in_progress_callback_ = std::move(callback); |
| } |
| } |
| |
| void FloatingSsoService::RunWhenCookiesAreReadyOnFirstSync( |
| base::OnceClosure callback) { |
| // base::Unretained() is safe since `bridge_` is owned by `this`. |
| bridge_->SetOnMergeFullSyncDataCallback( |
| base::BindOnce(&FloatingSsoService::RunWhenCookiesAreReady, |
| base::Unretained(this), std::move(callback))); |
| } |
| |
| void FloatingSsoService::MarkToNotOverride(const net::CanonicalCookie& cookie) { |
| std::optional<std::string> storage_key = SerializedKey(cookie); |
| if (storage_key) { |
| bridge_->AddToLocallyPreferredCookies(storage_key.value()); |
| } |
| } |
| |
| void FloatingSsoService::MaybeStartListening() { |
| if (!receiver_.is_bound()) { |
| BindToCookieManager(); |
| } |
| } |
| |
| void FloatingSsoService::StopListening() { |
| if (receiver_.is_bound()) { |
| // In case cookie listening will resume in the same session, make sure the |
| // accumulated cookie list will be fetched. |
| fetch_accumulated_cookies_ = true; |
| receiver_.reset(); |
| } |
| } |
| |
| void FloatingSsoService::BindToCookieManager() { |
| network::mojom::CookieManager* cookie_manager = cookie_manager_getter_.Run(); |
| if (!cookie_manager) { |
| return; |
| } |
| cookie_manager->AddGlobalChangeListener(receiver_.BindNewPipeAndPassRemote()); |
| receiver_.set_disconnect_handler(base::BindOnce( |
| &FloatingSsoService::OnConnectionError, base::Unretained(this))); |
| |
| if (fetch_accumulated_cookies_) { |
| cookie_manager->GetAllCookies(base::BindOnce( |
| &FloatingSsoService::OnCookiesLoaded, base::Unretained(this))); |
| } |
| } |
| |
| void FloatingSsoService::OnCookieChange(const net::CookieChangeInfo& change) { |
| const net::CanonicalCookie& cookie = change.cookie; |
| if (!ShouldSyncCookie(cookie)) { |
| return; |
| } |
| |
| switch (change.cause) { |
| case net::CookieChangeCause::INSERTED: |
| case net::CookieChangeCause::INSERTED_NO_CHANGE_OVERWRITE: { |
| bridge_->AddOrUpdateCookie(cookie); |
| break; |
| } |
| // All cases below correspond to deletion of a cookie. When intention is to |
| // update the cookie (e.g. in the case of `CookieChangeCause::OVERWRITE`), |
| // they will be immediately followed by a `CookieChangeCause::INSERTED` |
| // change. |
| case net::CookieChangeCause::EXPLICIT: |
| case net::CookieChangeCause::UNKNOWN_DELETION: |
| case net::CookieChangeCause::OVERWRITE: |
| case net::CookieChangeCause::EXPIRED: |
| case net::CookieChangeCause::EVICTED: |
| case net::CookieChangeCause::EXPIRED_OVERWRITE: |
| bridge_->DeleteCookie(cookie); |
| break; |
| } |
| } |
| |
| void FloatingSsoService::OnCookiesAddedOrUpdatedRemotely( |
| const std::vector<net::CanonicalCookie>& cookies) { |
| network::mojom::CookieManager* cookie_manager = cookie_manager_getter_.Run(); |
| net::CookieOptions options; |
| // Allow to alter http_only and SameSite cookies since we are restoring this |
| // cookie from another Chrome session. |
| options.set_include_httponly(); |
| options.set_same_site_cookie_context( |
| net::CookieOptions::SameSiteCookieContext::MakeInclusive()); |
| changes_in_progress_count_ += cookies.size(); |
| for (const net::CanonicalCookie& cookie : cookies) { |
| // Sync server might contain changes for cookies which should no longer be |
| // synced due to a change of policies or a change in feature design and |
| // implementation. In that case, ignore them on the client side and let |
| // corresponding sync entities die on the server side based on TTL . |
| if (!ShouldSyncCookie(cookie)) { |
| --changes_in_progress_count_; |
| continue; |
| } |
| cookie_manager->SetCanonicalCookie( |
| cookie, net::cookie_util::SimulatedCookieSource(cookie, "https"), |
| options, |
| base::BindOnce(&FloatingSsoService::OnCookieSet, |
| base::Unretained(this))); |
| } |
| } |
| |
| void FloatingSsoService::OnCookiesRemovedRemotely( |
| const std::vector<net::CanonicalCookie>& cookies) { |
| network::mojom::CookieManager* cookie_manager = cookie_manager_getter_.Run(); |
| changes_in_progress_count_ += cookies.size(); |
| for (const net::CanonicalCookie& cookie : cookies) { |
| // Sync server might contain changes for cookies which should no longer be |
| // synced due to a change of policies or a change in feature design and |
| // implementation. In that case, ignore them on the client side. |
| if (!ShouldSyncCookie(cookie)) { |
| --changes_in_progress_count_; |
| continue; |
| } |
| |
| cookie_manager->DeleteCanonicalCookie( |
| cookie, base::BindOnce(&FloatingSsoService::OnCookieDeleted, |
| base::Unretained(this))); |
| } |
| } |
| |
| void FloatingSsoService::OnCookiesLoaded(const net::CookieList& cookies) { |
| for (const net::CanonicalCookie& cookie : cookies) { |
| if (!ShouldSyncCookie(cookie)) { |
| continue; |
| } |
| bridge_->AddOrUpdateCookie(cookie); |
| } |
| } |
| |
| bool FloatingSsoService::ShouldSyncCookie( |
| const net::CanonicalCookie& cookie) const { |
| // We only sync cookies from HTTP headers because: |
| // 1) this should be enough to transfer auth-related cookies |
| // 2) JavaScript and/or extensions might update cookies much more frequently |
| // and we want to limit the number of Sync requests |
| if (cookie.SourceType() != net::CookieSourceType::kHTTP) { |
| return false; |
| } |
| |
| if (!cookie.IsPersistent() && !ShouldSyncSessionCookies()) { |
| return false; |
| } |
| |
| const GURL cookie_domain_url = net::cookie_util::CookieOriginToURL( |
| cookie.Domain(), cookie.SecureAttribute()); |
| return ShouldSyncCookiesForUrl(cookie_domain_url); |
| } |
| |
| bool FloatingSsoService::ShouldSyncSessionCookies() const { |
| const PrefService::Preference* session_cookies_pref = |
| prefs_->FindPreference(::prefs::kFloatingSsoSessionCookiesIncluded); |
| |
| // If the pref is null or the policy isn't managed, the FWS logic applies. |
| // Otherwise, the FloatingSsoSessionCookiesIncluded policy value directly |
| // determines if session cookies are synced, overriding any FWS logic. |
| if (session_cookies_pref && session_cookies_pref->IsManaged()) { |
| return session_cookies_pref->GetValue()->GetBool(); |
| } |
| |
| return ash::floating_workspace_util::IsFloatingWorkspaceV2Enabled(); |
| } |
| |
| bool FloatingSsoService::ShouldSyncCookiesForUrl(const GURL& url) const { |
| // Filter out Google cookies. |
| if (IsGoogleUrl(url)) { |
| return false; |
| } |
| // Filter out policy-blocked URLs. |
| if (!IsDomainAllowed(url)) { |
| return false; |
| } |
| return true; |
| } |
| |
| bool FloatingSsoService::IsDomainAllowed(const GURL& url) const { |
| bool is_excepted = !except_url_matcher_->MatchURL(url).empty(); |
| |
| // Exception list takes precedence. |
| if (is_excepted) { |
| return true; |
| } |
| |
| // The domain is not blocked if it doesn't have matches in the blocklist. |
| return block_url_matcher_->MatchURL(url).empty(); |
| } |
| |
| void FloatingSsoService::OnCookieSet(net::CookieAccessResult result) { |
| DecrementChangesCountAndMaybeNotify(); |
| } |
| |
| void FloatingSsoService::OnCookieDeleted(bool success) { |
| DecrementChangesCountAndMaybeNotify(); |
| } |
| |
| void FloatingSsoService::DecrementChangesCountAndMaybeNotify() { |
| CHECK(changes_in_progress_count_ > 0); |
| --changes_in_progress_count_; |
| if (changes_in_progress_count_ == 0 && on_no_changes_in_progress_callback_) { |
| std::move(on_no_changes_in_progress_callback_).Run(); |
| } |
| } |
| |
| void FloatingSsoService::OnConnectionError() { |
| // Don't fetch the accumulated cookies because we will try to reconnect right |
| // away. |
| fetch_accumulated_cookies_ = false; |
| receiver_.reset(); |
| MaybeStartListening(); |
| } |
| |
| base::WeakPtr<syncer::DataTypeControllerDelegate> |
| FloatingSsoService::GetControllerDelegate() { |
| return bridge_->change_processor()->GetControllerDelegate(); |
| } |
| |
| } // namespace ash::floating_sso |