blob: f1832e81a80757df65e4608589531cd18c1239d3 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/ntp_tiles/enterprise/ntp_shortcuts_policy_handler.h"
#include <string>
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "components/ntp_tiles/enterprise/enterprise_shortcuts_store.h"
#include "components/ntp_tiles/features.h"
#include "components/ntp_tiles/pref_names.h"
#include "components/policy/core/browser/policy_error_map.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_value_map.h"
#include "components/strings/grit/components_strings.h"
#include "url/gurl.h"
using ntp_tiles::EnterpriseShortcut;
using ntp_tiles::EnterpriseShortcutsStore;
namespace policy {
namespace {
bool IsNTPEnterpriseShortcutsEnabled() {
// Check that FeatureList is available as a protection against early startup
// crashes. Some policy providers are initialized very early even before
// base::FeatureList is available, but when policies are finally applied, the
// feature stack is fully initialized. The instance check ensures that the
// final decision is delayed until all features are initialized, without any
// other downstream effect.
return base::FeatureList::GetInstance() &&
base::FeatureList::IsEnabled(ntp_tiles::kNtpEnterpriseShortcuts);
}
// Converts a shortcuts policy entry `policy_dict` into a dictionary to be
// saved to prefs, with fields corresponding to `EnterpriseShortcut`. `CHECK`s
// are safe since this function is only used after policy values are validated.
base::Value NTPShortcutsDictFromPolicyValue(
const base::Value::Dict& policy_dict) {
base::Value::Dict dict;
// To align with `EnterpriseShortcut`, use "title" as dictionary key instead
// of "name".
const std::string* name =
policy_dict.FindString(NTPShortcutsPolicyHandler::kName);
CHECK(name);
dict.Set(EnterpriseShortcutsStore::kDictionaryKeyTitle, *name);
const std::string* url =
policy_dict.FindString(NTPShortcutsPolicyHandler::kUrl);
CHECK(url);
dict.Set(EnterpriseShortcutsStore::kDictionaryKeyUrl, *url);
dict.Set(EnterpriseShortcutsStore::kDictionaryKeyPolicyOrigin,
static_cast<int>(EnterpriseShortcut::PolicyOrigin::kNtpShortcuts));
dict.Set(EnterpriseShortcutsStore::kDictionaryKeyIsHiddenByUser, false);
const bool allow_user_edit =
policy_dict.FindBool(NTPShortcutsPolicyHandler::kAllowUserEdit)
.value_or(false);
dict.Set(EnterpriseShortcutsStore::kDictionaryKeyAllowUserEdit,
allow_user_edit);
const bool allow_user_delete =
policy_dict.FindBool(NTPShortcutsPolicyHandler::kAllowUserDelete)
.value_or(false);
dict.Set(EnterpriseShortcutsStore::kDictionaryKeyAllowUserDelete,
allow_user_delete);
return base::Value(std::move(dict));
}
bool UrlAlreadySeen(const std::string& policy_name,
const GURL& url,
const base::flat_set<GURL>& valid_urls_already_seen,
PolicyErrorMap* errors,
base::flat_set<GURL>* duplicated_urls) {
if (valid_urls_already_seen.find(url) == valid_urls_already_seen.end()) {
return false;
}
if (duplicated_urls->find(url) == duplicated_urls->end()) {
duplicated_urls->insert(url);
// Only show an warning message once per url.
errors->AddError(policy_name, IDS_POLICY_NTP_SHORTCUTS_DUPLICATED_URL,
url.spec(), {}, PolicyMap::MessageType::kWarning);
}
return true;
}
// Used for applying fake shortcuts for testing purposes only.
void ApplyFakeDataForManualTesting(PrefValueMap* prefs) {
base::Value::List shortcuts;
{
base::Value::Dict shortcut;
shortcut.Set("name", "Google Search");
shortcut.Set("url", "https://www.google.com/");
shortcuts.Append(NTPShortcutsDictFromPolicyValue(shortcut));
}
{
base::Value::Dict shortcut;
shortcut.Set("name", "Workday");
shortcut.Set("url", "https://workday.com");
shortcuts.Append(NTPShortcutsDictFromPolicyValue(shortcut));
}
{
base::Value::Dict shortcut;
shortcut.Set("name", "Concur");
shortcut.Set("url", "https://www.concur.com");
shortcuts.Append(NTPShortcutsDictFromPolicyValue(shortcut));
}
{
base::Value::Dict shortcut;
shortcut.Set("name", "JIRA");
shortcut.Set("url", "https://www.jira.com");
shortcuts.Append(NTPShortcutsDictFromPolicyValue(shortcut));
}
{
base::Value::Dict shortcut;
shortcut.Set("name", "Google Drive (E/D)");
shortcut.Set("url", "https://drive.google.com");
shortcut.Set("allow_user_edit", true);
shortcut.Set("allow_user_delete", true);
shortcuts.Append(NTPShortcutsDictFromPolicyValue(shortcut));
}
{
base::Value::Dict shortcut;
shortcut.Set("name", "Zoom (E/D)");
shortcut.Set("url", "https://zoom.com");
shortcut.Set("allow_user_edit", true);
shortcut.Set("allow_user_delete", true);
shortcuts.Append(NTPShortcutsDictFromPolicyValue(shortcut));
}
{
base::Value::Dict shortcut;
shortcut.Set("name", "Google Calendar (E)");
shortcut.Set("url", "https://calendar.google.com");
shortcut.Set("allow_user_edit", true);
shortcut.Set("allow_user_delete", false);
shortcuts.Append(NTPShortcutsDictFromPolicyValue(shortcut));
}
{
base::Value::Dict shortcut;
shortcut.Set("name", "Gmail (D)");
shortcut.Set("url", "https://mail.google.com");
shortcut.Set("allow_user_edit", false);
shortcut.Set("allow_user_delete", true);
shortcuts.Append(NTPShortcutsDictFromPolicyValue(shortcut));
}
prefs->SetValue(ntp_tiles::prefs::kEnterpriseShortcutsPolicyList,
base::Value(std::move(shortcuts)));
}
} // namespace
const char NTPShortcutsPolicyHandler::kName[] = "name";
const char NTPShortcutsPolicyHandler::kUrl[] = "url";
const char NTPShortcutsPolicyHandler::kAllowUserEdit[] = "allow_user_edit";
const char NTPShortcutsPolicyHandler::kAllowUserDelete[] = "allow_user_delete";
const int NTPShortcutsPolicyHandler::kMaxNtpShortcuts = 10;
const int NTPShortcutsPolicyHandler::kMaxNtpShortcutTextLength = 1000;
NTPShortcutsPolicyHandler::NTPShortcutsPolicyHandler(Schema schema)
: SimpleSchemaValidatingPolicyHandler(
key::kNTPShortcuts,
ntp_tiles::prefs::kEnterpriseShortcutsPolicyList,
schema,
policy::SchemaOnErrorStrategy::
SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY,
SimpleSchemaValidatingPolicyHandler::
RECOMMENDED_PROHIBITED, // Recommended policies are not supported
// since `allow_user_edit` and
// `allow_user_delete` fields allow for
// user modification.
SimpleSchemaValidatingPolicyHandler::MANDATORY_ALLOWED) {}
NTPShortcutsPolicyHandler::~NTPShortcutsPolicyHandler() = default;
bool NTPShortcutsPolicyHandler::CheckPolicySettings(const PolicyMap& policies,
PolicyErrorMap* errors) {
ignored_urls_.clear();
if (!IsNTPEnterpriseShortcutsEnabled() || !policies.Get(policy_name())) {
return true;
}
if (!SimpleSchemaValidatingPolicyHandler::CheckPolicySettings(policies,
errors)) {
return false;
}
const base::Value::List& shortcuts =
policies.GetValue(policy_name(), base::Value::Type::LIST)->GetList();
if (shortcuts.size() > kMaxNtpShortcuts) {
errors->AddError(policy_name(),
IDS_POLICY_NTP_SHORTCUTS_MAX_SHORTCUTS_LIMIT_ERROR,
base::NumberToString(kMaxNtpShortcuts));
return false;
}
base::flat_set<GURL> valid_urls_already_seen;
base::flat_set<GURL> duplicated_urls;
for (const base::Value& entry : shortcuts) {
const base::Value::Dict& dict = entry.GetDict();
const std::string* name = dict.FindString(kName);
const std::string* url_str = dict.FindString(kUrl);
// The `SCHEMA_ALLOW_UNKNOWN_AND_INVALID_LIST_ENTRY` strategy guarantees
// that `entry` is a dictionary, but its properties might be missing.
// A missing name or URL is a schema validation error, which is already
// reported as a generic warning.
if (!url_str) {
continue;
}
if (url_str->empty()) {
errors->AddError(policy_name(), IDS_SEARCH_POLICY_SETTINGS_URL_IS_EMPTY,
{}, PolicyMap::MessageType::kWarning);
continue;
}
GURL url(*url_str);
if (!url.is_valid()) {
errors->AddError(policy_name(), IDS_POLICY_INVALID_URL_ERROR, {},
PolicyMap::MessageType::kWarning);
ignored_urls_.insert(url);
continue;
}
if (url_str->length() > kMaxNtpShortcutTextLength) {
errors->AddError(policy_name(), IDS_POLICY_NTP_SHORTCUTS_URL_TOO_LONG,
base::NumberToString(kMaxNtpShortcutTextLength), {},
PolicyMap::MessageType::kWarning);
ignored_urls_.insert(url);
continue;
}
if (UrlAlreadySeen(policy_name(), url, valid_urls_already_seen, errors,
&duplicated_urls)) {
ignored_urls_.insert(url);
continue;
}
if (!name) {
ignored_urls_.insert(url);
continue;
}
if (name->empty()) {
errors->AddError(policy_name(), IDS_SEARCH_POLICY_SETTINGS_NAME_IS_EMPTY,
{}, PolicyMap::MessageType::kWarning);
ignored_urls_.insert(url);
continue;
}
if (name->length() > kMaxNtpShortcutTextLength) {
errors->AddError(policy_name(), IDS_POLICY_NTP_SHORTCUTS_NAME_TOO_LONG,
base::NumberToString(kMaxNtpShortcutTextLength), {},
PolicyMap::MessageType::kWarning);
ignored_urls_.insert(url);
continue;
}
valid_urls_already_seen.insert(url);
}
// Accept if there is at least one valid shortcut that has a unique URL
// (`valid_urls_already_seen` stores all valid urls including duplicate URLs).
if (valid_urls_already_seen.size() > duplicated_urls.size()) {
return true;
}
errors->AddError(policy_name(), IDS_POLICY_NTP_SHORTCUTS_NO_VALID_PROVIDER);
return false;
}
void NTPShortcutsPolicyHandler::ApplyPolicySettings(const PolicyMap& policies,
PrefValueMap* prefs) {
// If policy handler is disabled, the pref should be cleared to prevent old
// shortcuts from appearing.
if (!IsNTPEnterpriseShortcutsEnabled()) {
prefs->RemoveValue(ntp_tiles::prefs::kEnterpriseShortcutsPolicyList);
return;
}
if (ntp_tiles::kNtpEnterpriseShortcutsUseFakeDataParam.Get()) {
ApplyFakeDataForManualTesting(prefs);
return;
}
const base::Value* policy_value =
policies.GetValue(policy_name(), base::Value::Type::LIST);
if (!policy_value) {
prefs->RemoveValue(ntp_tiles::prefs::kEnterpriseShortcutsPolicyList);
return;
}
base::Value::List shortcuts;
for (const base::Value& item : policy_value->GetList()) {
const base::Value::Dict& policy_dict = item.GetDict();
const std::string* url_str = policy_dict.FindString(kUrl);
// An entry with a missing, empty, or invalid URL should be ignored.
if (!url_str) {
continue;
}
GURL url(*url_str);
if (!url.is_valid()) {
continue;
}
if (ignored_urls_.find(url) == ignored_urls_.end()) {
shortcuts.Append(NTPShortcutsDictFromPolicyValue(policy_dict));
}
}
prefs->SetValue(ntp_tiles::prefs::kEnterpriseShortcutsPolicyList,
base::Value(std::move(shortcuts)));
}
} // namespace policy