chromium/chrome/browser/ash/policy/core/device_local_account.cc

// Copyright 2013 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/policy/core/device_local_account.h"

#include <stddef.h>

#include <memory>
#include <optional>
#include <set>
#include <utility>

#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chrome/browser/ash/ownership/owner_settings_service_ash.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/user_names.h"
#include "google_apis/gaia/gaia_auth_util.h"

namespace policy {

namespace {

bool GetString(const base::Value::Dict& dict,
               const char* key,
               std::string* result) {
  const std::string* value = dict.FindString(key);
  if (!value) {
    return false;
  }
  *result = *value;
  return true;
}

bool IsKioskType(DeviceLocalAccountType type) {
  switch (type) {
    case DeviceLocalAccountType::kKioskApp:
    case DeviceLocalAccountType::kWebKioskApp:
    case DeviceLocalAccountType::kKioskIsolatedWebApp:
      return true;
    case DeviceLocalAccountType::kPublicSession:
    case DeviceLocalAccountType::kSamlPublicSession:
      return false;
  }
  NOTREACHED();
}

}  // namespace

WebKioskAppBasicInfo::WebKioskAppBasicInfo(const std::string& url,
                                           const std::string& title,
                                           const std::string& icon_url)
    : url_(url), title_(title), icon_url_(icon_url) {}

WebKioskAppBasicInfo::WebKioskAppBasicInfo() = default;

WebKioskAppBasicInfo::~WebKioskAppBasicInfo() = default;

IsolatedWebAppKioskBasicInfo::IsolatedWebAppKioskBasicInfo(
    std::string web_bundle_id,
    std::string update_manifest_url)
    : web_bundle_id_(std::move(web_bundle_id)),
      update_manifest_url_(std::move(update_manifest_url)) {}

DeviceLocalAccount::DeviceLocalAccount(DeviceLocalAccountType type,
                                       EphemeralMode ephemeral_mode,
                                       const std::string& account_id,
                                       const std::string& kiosk_app_id,
                                       const std::string& kiosk_app_update_url)
    : type(type),
      ephemeral_mode(ephemeral_mode),
      account_id(account_id),
      user_id(GenerateDeviceLocalAccountUserId(account_id, type)),
      kiosk_app_id(kiosk_app_id),
      kiosk_app_update_url(kiosk_app_update_url) {}

DeviceLocalAccount::DeviceLocalAccount(
    EphemeralMode ephemeral_mode,
    const WebKioskAppBasicInfo& web_kiosk_app_info,
    const std::string& account_id)
    : type(DeviceLocalAccountType::kWebKioskApp),
      ephemeral_mode(ephemeral_mode),
      account_id(account_id),
      user_id(GenerateDeviceLocalAccountUserId(account_id, type)),
      web_kiosk_app_info(web_kiosk_app_info) {}

DeviceLocalAccount::DeviceLocalAccount(
    EphemeralMode ephemeral_mode,
    const IsolatedWebAppKioskBasicInfo& kiosk_iwa_info,
    const std::string& account_id)
    : type(DeviceLocalAccountType::kKioskIsolatedWebApp),
      ephemeral_mode(ephemeral_mode),
      account_id(account_id),
      user_id(GenerateDeviceLocalAccountUserId(account_id, type)),
      kiosk_iwa_info(kiosk_iwa_info) {}

DeviceLocalAccount::DeviceLocalAccount(const DeviceLocalAccount& other) =
    default;

DeviceLocalAccount::~DeviceLocalAccount() = default;

std::vector<DeviceLocalAccount> GetDeviceLocalAccounts(
    ash::CrosSettings* cros_settings) {
  // TODO(crbug.com/40636049): handle TYPE_SAML_PUBLIC_SESSION
  std::vector<DeviceLocalAccount> accounts;

  const base::Value::List* list = nullptr;
  if (!cros_settings->GetList(ash::kAccountsPrefDeviceLocalAccounts, &list)) {
    return accounts;
  }

  std::set<std::string> account_ids;
  for (size_t i = 0; i < list->size(); ++i) {
    const base::Value& entry = (*list)[i];
    if (!entry.is_dict()) {
      LOG(ERROR) << "Corrupt entry in device-local account list at index " << i
                 << ".";
      continue;
    }

    const base::Value::Dict& entry_dict = entry.GetDict();
    std::string account_id;
    if (!GetString(entry_dict, ash::kAccountsPrefDeviceLocalAccountsKeyId,
                   &account_id) ||
        account_id.empty()) {
      LOG(ERROR) << "Missing account ID in device-local account list at index "
                 << i << ".";
      continue;
    }

    std::optional<int> raw_type =
        entry_dict.FindInt(ash::kAccountsPrefDeviceLocalAccountsKeyType);
    if (!raw_type || !IsValidDeviceLocalAccountType(*raw_type)) {
      LOG(ERROR) << "Missing or invalid account type in device-local account "
                 << "list at index " << i << ".";
      continue;
    }
    auto type = static_cast<DeviceLocalAccountType>(*raw_type);

    DeviceLocalAccount::EphemeralMode ephemeral_mode_value =
        DeviceLocalAccount::EphemeralMode::kUnset;
    if (IsKioskType(type)) {
      std::optional<int> ephemeral_mode = entry_dict.FindInt(
          ash::kAccountsPrefDeviceLocalAccountsKeyEphemeralMode);
      if (!ephemeral_mode || ephemeral_mode.value() < 0 ||
          ephemeral_mode.value() >
              static_cast<int>(DeviceLocalAccount::EphemeralMode::kMaxValue)) {
        LOG(ERROR) << "Missing or invalid ephemeral mode (value="
                   << ephemeral_mode.value_or(-1)
                   << ") in device-local account list at index " << i
                   << ", using default kUnset value for ephemeral mode.";
      } else {
        ephemeral_mode_value = static_cast<DeviceLocalAccount::EphemeralMode>(
            ephemeral_mode.value());
      }
    }

    if (!account_ids.insert(account_id).second) {
      LOG(ERROR) << "Duplicate entry in device-local account list at index "
                 << i << ": " << account_id << ".";
      continue;
    }

    switch (type) {
      case DeviceLocalAccountType::kPublicSession:
        accounts.emplace_back(DeviceLocalAccountType::kPublicSession,
                              ephemeral_mode_value, account_id, "", "");
        break;
      case DeviceLocalAccountType::kSamlPublicSession:
        accounts.emplace_back(DeviceLocalAccountType::kSamlPublicSession,
                              ephemeral_mode_value, account_id, "", "");
        break;
      case DeviceLocalAccountType::kKioskApp: {
        std::string kiosk_app_id;
        std::string kiosk_app_update_url;
        if (!GetString(entry_dict,
                       ash::kAccountsPrefDeviceLocalAccountsKeyKioskAppId,
                       &kiosk_app_id)) {
          LOG(ERROR) << "Missing app ID in device-local account entry at index "
                     << i << ".";
          continue;
        }
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyKioskAppUpdateURL,
                  &kiosk_app_update_url);

        accounts.emplace_back(DeviceLocalAccountType::kKioskApp,
                              ephemeral_mode_value, account_id, kiosk_app_id,
                              kiosk_app_update_url);
        break;
      }
      case DeviceLocalAccountType::kWebKioskApp: {
        std::string url;
        std::string title;
        std::string icon_url;
        if (!GetString(entry_dict,
                       ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskUrl,
                       &url)) {
          LOG(ERROR) << "Missing install url in Web kiosk type device-local "
                        "account at index "
                     << i << ".";
          continue;
        }

        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskTitle,
                  &title);
        GetString(entry_dict,
                  ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskIconUrl,
                  &icon_url);
        accounts.emplace_back(ephemeral_mode_value,
                              WebKioskAppBasicInfo(url, title, icon_url),
                              account_id);
        break;
      }
      case DeviceLocalAccountType::kKioskIsolatedWebApp: {
        std::string web_bundle_id;
        if (!GetString(entry_dict,
                       ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskBundleId,
                       &web_bundle_id)) {
          LOG(ERROR) << "Missing web bundle ID in IWA kiosk type device-local "
                        "account at index "
                     << i << ".";
          continue;
        }

        std::string update_manifest_url;
        if (!GetString(
                entry_dict,
                ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskUpdateUrl,
                &update_manifest_url)) {
          LOG(ERROR) << "Missing manifest url in IWA kiosk type device-local "
                        "account at index "
                     << i << ".";
          continue;
        }

        accounts.emplace_back(
            ephemeral_mode_value,
            IsolatedWebAppKioskBasicInfo(web_bundle_id, update_manifest_url),
            account_id);
        break;
      }
    }
  }
  return accounts;
}

void SetDeviceLocalAccountsForTesting(
    ash::OwnerSettingsServiceAsh* service,
    const std::vector<DeviceLocalAccount>& accounts) {
  // TODO(crbug.com/40636049): handle TYPE_SAML_PUBLIC_SESSION
  base::Value::List list;
  for (const auto& account : accounts) {
    auto entry =
        base::Value::Dict()
            .Set(ash::kAccountsPrefDeviceLocalAccountsKeyId, account.account_id)
            .Set(ash::kAccountsPrefDeviceLocalAccountsKeyType,
                 static_cast<int>(account.type))
            .Set(ash::kAccountsPrefDeviceLocalAccountsKeyEphemeralMode,
                 static_cast<int>(account.ephemeral_mode));
    switch (account.type) {
      case DeviceLocalAccountType::kPublicSession:
      case DeviceLocalAccountType::kSamlPublicSession:
        // Do nothing.
        break;
      case DeviceLocalAccountType::kKioskApp:
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyKioskAppId,
                  account.kiosk_app_id);
        if (!account.kiosk_app_update_url.empty()) {
          entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyKioskAppUpdateURL,
                    account.kiosk_app_update_url);
        }
        break;
      case DeviceLocalAccountType::kWebKioskApp:
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskUrl,
                  account.web_kiosk_app_info.url());
        if (!account.web_kiosk_app_info.title().empty()) {
          entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskTitle,
                    account.web_kiosk_app_info.title());
        }
        if (!account.web_kiosk_app_info.icon_url().empty()) {
          entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyWebKioskIconUrl,
                    account.web_kiosk_app_info.icon_url());
        }
        break;
      case DeviceLocalAccountType::kKioskIsolatedWebApp:
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskBundleId,
                  account.kiosk_iwa_info.web_bundle_id());
        entry.Set(ash::kAccountsPrefDeviceLocalAccountsKeyIwaKioskUpdateUrl,
                  account.kiosk_iwa_info.update_manifest_url());
        break;
    }
    list.Append(std::move(entry));
  }

  service->Set(ash::kAccountsPrefDeviceLocalAccounts,
               base::Value(std::move(list)));
}

}  // namespace policy