// 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/enterprise/connectors/connectors_service.h"

#include <memory>
#include <variant>

#include "base/check_op.h"
#include "base/no_destructor.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/enterprise/browser_management/management_identity.h"
#include "chrome/browser/enterprise/connectors/common.h"
#include "chrome/browser/enterprise/connectors/connectors_manager.h"
#include "chrome/browser/policy/chrome_browser_policy_connector.h"
#include "chrome/browser/policy/dm_token_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/reporting_util.h"
#include "chrome/browser/safe_browsing/cloud_content_scanning/deep_scanning_utils.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "components/enterprise/browser/controller/browser_dm_token_storage.h"
#include "components/enterprise/buildflags/buildflags.h"
#include "components/enterprise/common/proto/connectors.pb.h"
#include "components/enterprise/connectors/core/connectors_manager_base.h"
#include "components/enterprise/connectors/core/connectors_prefs.h"
#include "components/enterprise/connectors/core/service_provider_config.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "components/policy/core/common/cloud/cloud_policy_store.h"
#include "components/policy/core/common/cloud/dm_token.h"
#include "components/policy/core/common/cloud/machine_level_user_cloud_policy_manager.h"
#include "components/policy/core/common/cloud/machine_level_user_cloud_policy_store.h"
#include "components/policy/core/common/policy_types.h"
#include "components/safe_browsing/buildflags.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/user_prefs/user_prefs.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_context.h"
#include "content/public/common/url_constants.h"
#include "device_management_backend.pb.h"
#include "google_apis/gaia/gaia_auth_util.h"

#if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS)
#include "chrome/browser/extensions/chrome_content_browser_client_extensions_part.h"  // nogncheck crbug.com/40147906
#include "extensions/browser/extension_registry_factory.h"  // nogncheck crbug.com/40147906
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/policy/core/user_cloud_policy_manager_ash.h"
#include "chrome/browser/ash/settings/device_settings_service.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/components/mgs/managed_guest_session_utils.h"
#include "components/user_manager/user_manager.h"
#include "extensions/common/constants.h"
#else
#include "components/policy/core/common/cloud/profile_cloud_policy_manager.h"
#endif

namespace enterprise_connectors {

namespace {

std::string GetClientId(Profile* profile) {
  std::string client_id;
#if BUILDFLAG(IS_CHROMEOS)
  auto* manager = profile->GetUserCloudPolicyManagerAsh();
  if (manager && manager->core() && manager->core()->client()) {
    client_id = manager->core()->client()->client_id();
  }
#else
  client_id = policy::BrowserDMTokenStorage::Get()->RetrieveClientId();
#endif
  return client_id;
}

std::string GetDeviceClientId(Profile* profile) {
#if BUILDFLAG(IS_CHROMEOS)
  auto* device_settings_service = ash::DeviceSettingsService::Get();
  const auto* policy_data = device_settings_service->policy_data();
  if (policy_data && policy_data->has_device_id()) {
    return policy_data->device_id();
  }
#endif
  // This actually won't return the device client ID for ChromeOS, it's just
  // a fallback in that case.
  return GetClientId(profile);
}

std::string MaybeGetProfileEmail(Profile* profile) {
#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
  if (!identity_manager) {
    return std::string();
  }

  return GetProfileEmail(identity_manager);
#else
  return std::string();
#endif
}

#if BUILDFLAG(IS_CHROMEOS)
std::optional<std::string> GetDeviceDMToken() {
  const enterprise_management::PolicyData* policy_data =
      ash::DeviceSettingsService::Get()->policy_data();
  if (policy_data && policy_data->has_request_token()) {
    return policy_data->request_token();
  }
  return std::nullopt;
}
#endif

bool IsManagedGuestSession() {
#if BUILDFLAG(IS_CHROMEOS)
  return chromeos::IsManagedGuestSession();
#else
  return false;
#endif
}
}  // namespace

// --------------------------------
// ConnectorsService implementation
// --------------------------------

ConnectorsService::ConnectorsService(
    content::BrowserContext* context,
    std::unique_ptr<ConnectorsManagerBase> manager)
    : ConnectorsServiceBase(std::move(manager)), context_(context) {
  DCHECK(context_);
}

ConnectorsService::~ConnectorsService() = default;

std::unique_ptr<ClientMetadata> ConnectorsService::GetBasicClientMetadata(
    Profile* profile) {
  auto metadata = std::make_unique<ClientMetadata>();
  // We need to return profile and browser DM tokens, even in cases where the
  // reporting policy is disabled, in order to support merging rules.
  std::optional<std::string> browser_dm_token = GetBrowserDmToken();
  if (browser_dm_token.has_value()) {
    metadata->mutable_device()->set_dm_token(*browser_dm_token);
  }

  std::optional<std::string> profile_dm_token =
      reporting::GetUserDmToken(profile);
  if (profile_dm_token.has_value()) {
    metadata->mutable_profile()->set_dm_token(*profile_dm_token);
  }

  // In this case, we are just using the client metadata to indicate to
  // WebProtect whether or not the request is coming from a Managed Guest
  // Session on ChromeOS.
  metadata->set_is_chrome_os_managed_guest_session(IsManagedGuestSession());
  return metadata;
}

std::optional<ReportingSettings> ConnectorsService::GetReportingSettings() {
#if BUILDFLAG(IS_CHROMEOS)
  if (!ConnectorsEnabled()) {
    return std::nullopt;
  }

  std::optional<ReportingSettings> settings =
      connectors_manager_base_->GetReportingSettings();
  if (!settings.has_value()) {
    return std::nullopt;
  }

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  Profile* profile = Profile::FromBrowserContext(context_);
  if (IncludeDeviceInfo(profile, /*per_profile=*/false)) {
    // The device dm token includes additional information like a device id,
    // which is relevant for reporting and should only be used for
    // IncludeDeviceInfo==true.
    std::optional<std::string> device_dm_token = GetDeviceDMToken();
    if (device_dm_token.has_value()) {
      settings.value().dm_token = device_dm_token.value();
      settings.value().per_profile = false;
      return settings;
    }
  }
#endif  // BUILDFLAG(SAFE_BROWSING_AVAILABLE)
#endif  // BUILDFLAG(IS_CHROMEOS)

  return ConnectorsServiceBase::GetReportingSettings();
}

#if BUILDFLAG(IS_CHROMEOS)
std::optional<AnalysisSettings> ConnectorsService::GetAnalysisSettings(
    const storage::FileSystemURL& source_url,
    const storage::FileSystemURL& destination_url,
    AnalysisConnector connector) {
  DCHECK_EQ(connector, AnalysisConnector::FILE_TRANSFER);
  if (!ConnectorsEnabled()) {
    return std::nullopt;
  }

  auto* connectors_manager =
      static_cast<ConnectorsManager*>(connectors_manager_base_.get());

  return GetCommonAnalysisSettings(
      connectors_manager->GetAnalysisSettings(context_, source_url,
                                              destination_url, connector),
      connector);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

std::string ConnectorsService::GetManagementDomain() {
  if (!ConnectorsEnabled()) {
    return std::string();
  }

  std::optional<policy::PolicyScope> scope;
  for (const char* scope_pref :
       {enterprise_connectors::kEnterpriseRealTimeUrlCheckScope,
#if !BUILDFLAG(IS_ANDROID)
        AnalysisConnectorScopePref(AnalysisConnector::FILE_ATTACHED),
#endif
        AnalysisConnectorScopePref(AnalysisConnector::FILE_DOWNLOADED),
#if !BUILDFLAG(IS_ANDROID)
        AnalysisConnectorScopePref(AnalysisConnector::BULK_DATA_ENTRY),
        AnalysisConnectorScopePref(AnalysisConnector::PRINT),
#endif
        kOnSecurityEventScopePref}) {
    std::optional<DmToken> dm_token = GetDmToken(scope_pref);
    if (dm_token.has_value()) {
      scope = dm_token.value().scope;

      // Having one CBCM Connector policy set implies that profile ones will be
      // ignored for another domain, so the loop can stop immediately.
      if (scope == policy::PolicyScope::POLICY_SCOPE_MACHINE) {
        break;
      }
    }
  }

  if (!scope.has_value()) {
    return std::string();
  }

#if BUILDFLAG(IS_CHROMEOS)
  return GetAccountManagerIdentity(Profile::FromBrowserContext(context_))
      .value_or(std::string());
#else
  if (scope.value() == policy::PolicyScope::POLICY_SCOPE_USER) {
    return GetAccountManagerIdentity(Profile::FromBrowserContext(context_))
        .value_or(std::string());
  }

  policy::MachineLevelUserCloudPolicyManager* manager =
      g_browser_process->browser_policy_connector()
          ->machine_level_user_cloud_policy_manager();
  if (!manager) {
    return std::string();
  }

  policy::CloudPolicyStore* store = manager->store();
  return (store && store->has_policy())
             ? gaia::ExtractDomainName(store->policy()->username())
             : std::string();
#endif
}

bool ConnectorsService::IsProfileAffiliated() const {
  return IsAffiliated(Profile::FromBrowserContext(context_));
}

std::string ConnectorsService::GetProfileEmail() const {
  return MaybeGetProfileEmail(Profile::FromBrowserContext(context_));
}

std::string ConnectorsService::GetDeviceClientId() const {
  return ::enterprise_connectors::GetDeviceClientId(
      Profile::FromBrowserContext(context_));
}

std::optional<ConnectorsService::DmToken> ConnectorsService::GetDmToken(
    const char* scope_pref) const {
#if BUILDFLAG(IS_CHROMEOS)
  // On CrOS the settings from primary profile applies to all profiles.
  auto dm_token = GetBrowserDmToken();
  return dm_token ? std::make_optional<DmToken>(*dm_token,
                                                policy::POLICY_SCOPE_MACHINE)
                  : std::nullopt;
#else
  auto browser_dm_token = GetBrowserDmToken();
  policy::PolicyScope scope = GetPolicyScope(scope_pref);
  std::string token_string = scope == policy::POLICY_SCOPE_USER
                                 ? GetProfileDmToken().value_or("")
                                 : browser_dm_token.value_or("");
  if (token_string.empty()) {
    return std::nullopt;
  }
  return DmToken(token_string, scope);
#endif
}

std::optional<std::string> ConnectorsService::GetBrowserDmToken() const {
  policy::DMToken dm_token =
      policy::GetDMToken(Profile::FromBrowserContext(context_));

  if (!dm_token.is_valid()) {
    return std::nullopt;
  }

  return dm_token.value();
}

policy::PolicyScope ConnectorsService::GetPolicyScope(
    const char* scope_pref) const {
#if BUILDFLAG(IS_CHROMEOS)
  // CrOS always uses a browser DM throughout connectors code, so its policy
  // scope should always be POLICY_SCOPE_MACHINE.
  return policy::PolicyScope::POLICY_SCOPE_MACHINE;
#else
  return ConnectorsServiceBase::GetPolicyScope(scope_pref);
#endif
}

bool ConnectorsService::ConnectorsEnabled() const {
  Profile* profile = Profile::FromBrowserContext(context_);

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
  // On desktop, the guest profile is actually the primary OTR profile of
  // the "regular" guest profile.  The regular guest profile is never used
  // directly by users.  Also, user are not able to create child OTR profiles
  // from guest profiles, the menu item "New incognito window" is not
  // available.  So, if this is a guest session, allow it only if it is a
  // child OTR profile as well.
  if (profile->IsGuestSession()) {
    return profile->GetOriginalProfile() != profile;
  }

  // Never allow system profiles.
  if (profile->IsSystemProfile()) {
    return false;
  }
#endif

  return !profile->IsOffTheRecord() || profile->IsGuestSession();
}

PrefService* ConnectorsService::GetPrefs() {
  return Profile::FromBrowserContext(context_)->GetPrefs();
}

const PrefService* ConnectorsService::GetPrefs() const {
  return Profile::FromBrowserContext(context_)->GetPrefs();
}

policy::CloudPolicyManager*
ConnectorsService::GetManagedUserCloudPolicyManager() const {
  return Profile::FromBrowserContext(context_)->GetCloudPolicyManager();
}

std::unique_ptr<ClientMetadata> ConnectorsService::BuildClientMetadata(
    bool is_cloud) {
  auto reporting_settings = GetReportingSettings();

  Profile* profile = Profile::FromBrowserContext(context_);
  if (is_cloud && !reporting_settings.has_value()) {
    return GetBasicClientMetadata(profile);
  }

  auto metadata = std::make_unique<ClientMetadata>(
      reporting::GetContextAsClientMetadata(profile));

  // Device info is only useful for cloud service providers since local
  // providers can already determine all this info themselves. For this reason,
  // we only include browser metadata.
  if (!is_cloud) {
    PopulateBrowserMetadata(/*include_device_info=*/true,
                            metadata->mutable_browser());
    return metadata;
  }

  metadata->set_is_chrome_os_managed_guest_session(IsManagedGuestSession());

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  bool include_device_info =
      IncludeDeviceInfo(profile, reporting_settings.value().per_profile);

  PopulateBrowserMetadata(include_device_info, metadata->mutable_browser());

  if (include_device_info) {
    PopulateDeviceMetadata(GetClientId(profile), metadata->mutable_device());
  }
#endif

  return metadata;
}

bool ConnectorsService::IsURLExemptFromAnalysis(const GURL& url,
                                                AnalysisConnector connector) {
#if BUILDFLAG(IS_CHROMEOS)
  if (url.SchemeIs(extensions::kExtensionScheme) &&
      extension_misc::IsSystemUIApp(url.host())) {
    return true;
  }
#endif

  return ConnectorsServiceBase::IsURLExemptFromAnalysis(url, connector);
}

// ---------------------------------------
// ConnectorsServiceFactory implementation
// ---------------------------------------

// static
ConnectorsServiceFactory* ConnectorsServiceFactory::GetInstance() {
  static base::NoDestructor<ConnectorsServiceFactory> instance;
  return instance.get();
}

ConnectorsService* ConnectorsServiceFactory::GetForBrowserContext(
    content::BrowserContext* context) {
  return static_cast<ConnectorsService*>(
      GetInstance()->GetServiceForBrowserContext(context, true));
}

ConnectorsServiceFactory::ConnectorsServiceFactory()
    : BrowserContextKeyedServiceFactory(
          "ConnectorsService",
          BrowserContextDependencyManager::GetInstance()) {
#if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS)
  DependsOn(extensions::ExtensionRegistryFactory::GetInstance());
#endif
}

ConnectorsServiceFactory::~ConnectorsServiceFactory() = default;

std::unique_ptr<KeyedService>
ConnectorsServiceFactory::BuildServiceInstanceForBrowserContext(
    content::BrowserContext* context) const {
  return std::make_unique<ConnectorsService>(
      context,
      std::make_unique<ConnectorsManager>(user_prefs::UserPrefs::Get(context),
                                          GetServiceProviderConfig()));
}

content::BrowserContext* ConnectorsServiceFactory::GetBrowserContextToUse(
    content::BrowserContext* context) const {
#if BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS)
  // Do not construct the connectors service if the extensions are disabled for
  // the given context.
  if (extensions::ChromeContentBrowserClientExtensionsPart::
          AreExtensionsDisabledForProfile(context)) {
    return nullptr;
  }
#endif

  // On Chrome OS, settings from the primary/main profile apply to all
  // profiles, besides incognito.
  // However, the primary/main profile might not exist in tests - then the
  // provided |context| is still used.
  if (context && !context->IsOffTheRecord() &&
      !Profile::FromBrowserContext(context)->AsTestingProfile() &&
      !context->ShutdownStarted()) {
#if BUILDFLAG(IS_CHROMEOS)
    auto* user_manager = user_manager::UserManager::Get();
    if (auto* primary_user = user_manager->GetPrimaryUser()) {
      if (auto* primary_browser_context =
              ash::BrowserContextHelper::Get()->GetBrowserContextByUser(
                  primary_user)) {
        return primary_browser_context;
      }
    }
#endif
  }
  return context;
}

}  // namespace enterprise_connectors
