#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);
}
}
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)));
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() {
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(), false, &block_id, blocklist);
}
if (!blocklist_exceptions.empty()) {
base::MatcherStringPattern::ID except_id = 0;
url_matcher::util::AddFiltersWithLimit(except_url_matcher_.get(),
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() {
if (!prefs_->GetBoolean(chromeos::prefs::kFloatingSsoEnabled)) {
return false;
}
if (prefs_->GetBoolean(syncer::prefs::internal::kSyncManaged)) {
return false;
}
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) {
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()) {
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:
case net::CookieChangeCause::INSERTED_NO_VALUE_CHANGE_OVERWRITE: {
bridge_->AddOrUpdateCookie(cookie);
break;
}
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;
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) {
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) {
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 {
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 (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 {
if (IsGoogleUrl(url)) {
return false;
}
if (!IsDomainAllowed(url)) {
return false;
}
return true;
}
bool FloatingSsoService::IsDomainAllowed(const GURL& url) const {
bool is_excepted = !except_url_matcher_->MatchURL(url).empty();
if (is_excepted) {
return true;
}
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() {
fetch_accumulated_cookies_ = false;
receiver_.reset();
MaybeStartListening();
}
base::WeakPtr<syncer::DataTypeControllerDelegate>
FloatingSsoService::GetControllerDelegate() {
return bridge_->change_processor()->GetControllerDelegate();
}
}