Add WebUsbAllowDevicesForUrls policy

This change adds a new policy template for WebUsbAllowDevicesForUrls.
The new policy will allow a list of devices to be automatically granted
permission to be used for a list of URL patterns. This change also adds
a policy handler to validate the policy value and add the validated
value to Preferences using the pref name defined for this policy.

The design document for the new WebUSB policy is located at:
https://docs.google.com/document/d/1MPvsrWiVD_jAC8ELyk8njFpy6j1thfVU5aWT3TCWE8w/

Bug: 854329
Change-Id: I03657906834ba12f43735d7339a8ca404dc50718
Reviewed-on: https://chromium-review.googlesource.com/1225692
Commit-Queue: Ovidio Henriquez <odejesush@chromium.org>
Reviewed-by: Jochen Eisinger <jochen@chromium.org>
Reviewed-by: Sergey Poromov <poromov@chromium.org>
Cr-Commit-Position: refs/heads/master@{#595880}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index e6df9c5..47ea6ba 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1106,6 +1106,8 @@
     "policy/schema_registry_service.h",
     "policy/schema_registry_service_factory.cc",
     "policy/schema_registry_service_factory.h",
+    "policy/webusb_allow_devices_for_urls_policy_handler.cc",
+    "policy/webusb_allow_devices_for_urls_policy_handler.h",
     "predictors/autocomplete_action_predictor.cc",
     "predictors/autocomplete_action_predictor.h",
     "predictors/autocomplete_action_predictor_factory.cc",
diff --git a/chrome/browser/policy/configuration_policy_handler_list_factory.cc b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
index df749362..bc61fad 100644
--- a/chrome/browser/policy/configuration_policy_handler_list_factory.cc
+++ b/chrome/browser/policy/configuration_policy_handler_list_factory.cc
@@ -22,6 +22,7 @@
 #include "chrome/browser/policy/javascript_policy_handler.h"
 #include "chrome/browser/policy/managed_bookmarks_policy_handler.h"
 #include "chrome/browser/policy/network_prediction_policy_handler.h"
+#include "chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.h"
 #include "chrome/browser/profiles/force_safe_search_policy_handler.h"
 #include "chrome/browser/profiles/force_youtube_safety_mode_policy_handler.h"
 #include "chrome/browser/profiles/guest_mode_policy_handler.h"
@@ -994,6 +995,8 @@
       SCHEMA_STRICT,
       SimpleSchemaValidatingPolicyHandler::RECOMMENDED_PROHIBITED,
       SimpleSchemaValidatingPolicyHandler::MANDATORY_ALLOWED));
+  handlers->AddHandler(
+      std::make_unique<WebUsbAllowDevicesForUrlsPolicyHandler>(chrome_schema));
 
 // On most platforms, there is a legacy policy
 // kUnsafelyTreatInsecureOriginAsSecure which has been replaced by
diff --git a/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.cc b/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.cc
new file mode 100644
index 0000000..a68ee11
--- /dev/null
+++ b/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.cc
@@ -0,0 +1,130 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.h"
+
+#include "base/strings/stringprintf.h"
+#include "base/values.h"
+#include "chrome/common/pref_names.h"
+#include "components/content_settings/core/common/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"
+
+namespace policy {
+
+namespace {
+
+constexpr char kDevicesKey[] = "devices";
+constexpr char kVendorIdKey[] = "vendor_id";
+constexpr char kProductIdKey[] = "product_id";
+constexpr char kErrorPathTemplate[] = "items[%d].devices.items[%d]";
+constexpr char kInvalidUnsignedShortIntErrorTemplate[] =
+    "The %s must be an unsigned short integer";
+constexpr char kMissingVendorIdError[] = "A vendor_id must also be specified";
+
+}  // namespace
+
+WebUsbAllowDevicesForUrlsPolicyHandler::WebUsbAllowDevicesForUrlsPolicyHandler(
+    Schema schema)
+    : SchemaValidatingPolicyHandler(
+          key::kWebUsbAllowDevicesForUrls,
+          schema.GetKnownProperty(key::kWebUsbAllowDevicesForUrls),
+          SchemaOnErrorStrategy::SCHEMA_STRICT) {}
+
+WebUsbAllowDevicesForUrlsPolicyHandler::
+    ~WebUsbAllowDevicesForUrlsPolicyHandler() {}
+
+bool WebUsbAllowDevicesForUrlsPolicyHandler::CheckPolicySettings(
+    const PolicyMap& policies,
+    PolicyErrorMap* errors) {
+  const base::Value* value = policies.GetValue(policy_name());
+  if (!value)
+    return true;
+
+  bool result =
+      SchemaValidatingPolicyHandler::CheckPolicySettings(policies, errors);
+
+  std::string error_path;
+  std::string error;
+  if (result) {
+    // The vendor and product ID descriptors of a USB devices should be
+    // unsigned short integers.
+    int item_index = 0;
+    int device_index = 0;
+    for (const auto& item : value->GetList()) {
+      DCHECK(item.FindKey(kDevicesKey));
+
+      for (const auto& device : item.FindKey(kDevicesKey)->GetList()) {
+        if (device.FindKey(kVendorIdKey)) {
+          DCHECK(device.FindKey(kVendorIdKey)->is_int());
+
+          const int vendor_id = device.FindKey(kVendorIdKey)->GetInt();
+          if (vendor_id > 0xFFFF || vendor_id < 0) {
+            error_path = base::StringPrintf(kErrorPathTemplate, item_index,
+                                            device_index);
+            error = base::StringPrintf(kInvalidUnsignedShortIntErrorTemplate,
+                                       kVendorIdKey);
+            result = false;
+            break;
+          }
+        }
+
+        if (device.FindKey(kProductIdKey)) {
+          // If a |product_id| is specified, then a |vendor_id| must also be
+          // specified. Otherwise, the device policy is invalid.
+          if (device.FindKey(kVendorIdKey)) {
+            DCHECK(device.FindKey(kProductIdKey)->is_int());
+
+            const int product_id = device.FindKey(kProductIdKey)->GetInt();
+            if (product_id > 0xFFFF || product_id < 0) {
+              error_path = base::StringPrintf(kErrorPathTemplate, item_index,
+                                              device_index);
+              error = base::StringPrintf(kInvalidUnsignedShortIntErrorTemplate,
+                                         kProductIdKey);
+              result = false;
+              break;
+            }
+          } else {
+            error_path = base::StringPrintf(kErrorPathTemplate, item_index,
+                                            device_index);
+            error = kMissingVendorIdError;
+            result = false;
+            break;
+          }
+        }
+        ++device_index;
+      }
+
+      if (!error_path.empty() || error.empty())
+        break;
+
+      ++item_index;
+    }
+
+    if (errors && !error.empty()) {
+      if (error_path.empty())
+        error_path = "(ROOT)";
+      errors->AddError(policy_name(), error_path, error);
+    }
+  }
+
+  return result;
+}
+
+void WebUsbAllowDevicesForUrlsPolicyHandler::ApplyPolicySettings(
+    const PolicyMap& policies,
+    PrefValueMap* prefs) {
+  std::unique_ptr<base::Value> value;
+  if (!CheckAndGetValue(policies, nullptr, &value))
+    return;
+
+  if (!value || !value->is_list())
+    return;
+
+  prefs->SetValue(prefs::kManagedWebUsbAllowDevicesForUrls, std::move(value));
+}
+
+}  // namespace policy
diff --git a/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.h b/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.h
new file mode 100644
index 0000000..9580c4c
--- /dev/null
+++ b/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.h
@@ -0,0 +1,36 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_POLICY_WEBUSB_ALLOW_DEVICES_FOR_URLS_POLICY_HANDLER_H_
+#define CHROME_BROWSER_POLICY_WEBUSB_ALLOW_DEVICES_FOR_URLS_POLICY_HANDLER_H_
+
+#include "base/macros.h"
+#include "components/policy/core/browser/configuration_policy_handler.h"
+
+class PrefValueMap;
+
+namespace policy {
+
+class PolicyMap;
+
+// Handles the WebUsbAllowDevicesForUrls policy.
+class WebUsbAllowDevicesForUrlsPolicyHandler
+    : public SchemaValidatingPolicyHandler {
+ public:
+  explicit WebUsbAllowDevicesForUrlsPolicyHandler(Schema schema);
+  ~WebUsbAllowDevicesForUrlsPolicyHandler() override;
+
+  // ConfigurationPolicyHandler implementation:
+  bool CheckPolicySettings(const PolicyMap& policies,
+                           PolicyErrorMap* error) override;
+  void ApplyPolicySettings(const PolicyMap& policies,
+                           PrefValueMap* prefs) override;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(WebUsbAllowDevicesForUrlsPolicyHandler);
+};
+
+}  // namespace policy
+
+#endif  // CHROME_BROWSER_POLICY_WEBUSB_ALLOW_DEVICES_FOR_URLS_POLICY_HANDLER_H_
diff --git a/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler_unittest.cc b/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler_unittest.cc
new file mode 100644
index 0000000..53d0693
--- /dev/null
+++ b/chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler_unittest.cc
@@ -0,0 +1,539 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/policy/webusb_allow_devices_for_urls_policy_handler.h"
+
+#include <memory>
+
+#include "base/json/json_reader.h"
+#include "base/strings/string_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "components/content_settings/core/common/pref_names.h"
+#include "components/policy/core/browser/configuration_policy_pref_store.h"
+#include "components/policy/core/browser/configuration_policy_pref_store_test.h"
+#include "components/policy/core/browser/policy_error_map.h"
+#include "components/policy/core/common/policy_map.h"
+#include "components/policy/core/common/policy_types.h"
+#include "components/policy/core/common/schema.h"
+#include "components/policy/policy_constants.h"
+
+namespace policy {
+
+namespace {
+
+const char kDevicesKey[] = "devices";
+const char kUrlPatternsKey[] = "url_patterns";
+const char kVendorIdKey[] = "vendor_id";
+const char kProductIdKey[] = "product_id";
+const char kValidPolicy[] =
+    "["
+    "  {"
+    "    \"devices\": ["
+    // Ensure that a device can have both IDs.
+    "      {"
+    "        \"vendor_id\": 1234,"
+    "        \"product_id\": 5678"
+    // Ensure that a device can have only a
+    // |vendor_id|.
+    "      }, {"
+    "        \"vendor_id\": 4321"
+    "      }"
+    "    ],"
+    "    \"url_patterns\": ["
+    "      \"[*.]google.com\","
+    "      \"youtube.com\""
+    "    ]"
+    "  }, {"
+    // Ensure that a device can have neither IDs.
+    "    \"devices\": [{ }],"
+    "    \"url_patterns\": [\"[*.]crbug.com\"]"
+    "  }"
+    "]";
+// An invalid entry invalidates the entire policy.
+const char kInvalidPolicyInvalidTopLevelEntry[] =
+    "["
+    "  {"
+    "    \"devices\": ["
+    "      {"
+    "        \"vendor_id\": 1234,"
+    "        \"product_id\": 5678"
+    "      }, {"
+    "        \"vendor_id\": 4321"
+    "      }"
+    "    ],"
+    "    \"url_patterns\": ["
+    "      \"[*.]google.com\","
+    "      \"youtube.com\""
+    "    ]"
+    "  }, {"
+    "    \"url_patterns\": [\"[*.]crbug.com\"]"
+    "  }"
+    "]";
+// A list item must have both |devices| and
+// |url_patterns| specified.
+const char kInvalidPolicyMissingDevicesProperty[] =
+    "["
+    "  {"
+    "    \"url_patterns\": ["
+    "      \"[*.]google.com\","
+    "      \"youtube.com\""
+    "    ]"
+    "  }"
+    "]";
+const char kInvalidPolicyMissingUrlPatternsProperty[] =
+    "["
+    "  {"
+    "    \"devices\": ["
+    "      {"
+    "        \"vendor_id\": 1234,"
+    "        \"product_id\": 5678"
+    "      }"
+    "    ]"
+    "  }"
+    "]";
+// The |vendor_id| and |product_id| values should fit into an unsigned short.
+const char kInvalidPolicyMismatchedVendorIdType[] =
+    "["
+    "  {"
+    "    \"devices\": ["
+    "      {"
+    "        \"vendor_id\": 70000,"
+    "        \"product_id\": 5678"
+    "      }"
+    "    ],"
+    "    \"url_patterns\": ["
+    "      \"[*.]google.com\","
+    "      \"youtube.com\""
+    "    ]"
+    "  }"
+    "]";
+const char kInvalidPolicyMismatchedProductIdType[] =
+    "["
+    "  {"
+    "    \"devices\": ["
+    "      {"
+    "        \"vendor_id\": 1234,"
+    "        \"product_id\": 70000"
+    "      }"
+    "    ],"
+    "    \"url_patterns\": ["
+    "      \"[*.]google.com\","
+    "      \"youtube.com\""
+    "    ]"
+    "  }"
+    "]";
+// Unknown properties invalidate the policy.
+const char kInvalidPolicyUnknownProperty[] =
+    "["
+    "  {"
+    "    \"devices\": ["
+    "      {"
+    "        \"vendor_id\": 1234,"
+    "        \"product_id\": 5678,"
+    "        \"serialNumber\": \"1234ABCD\""
+    "      }"
+    "    ],"
+    "    \"url_patterns\": ["
+    "      \"[*.]google.com\","
+    "      \"youtube.com\""
+    "    ]"
+    "  }"
+    "]";
+// A device containing a |product_id| must also have a |vendor_id|.
+const char kInvalidPolicyProductIdWithoutVendorId[] =
+    "["
+    "  {"
+    "    \"devices\": ["
+    "      {"
+    "        \"product_id\": 5678"
+    "      }"
+    "    ],"
+    "    \"url_patterns\": ["
+    "      \"[*.]google.com\","
+    "      \"youtube.com\""
+    "    ]"
+    "  }"
+    "]";
+
+}  // namespace
+
+class WebUsbAllowDevicesForUrlsPolicyHandlerTest
+    : public ConfigurationPolicyPrefStoreTest {
+ public:
+  WebUsbAllowDevicesForUrlsPolicyHandler* handler() { return handler_; }
+
+ private:
+  void SetUp() override {
+    Schema chrome_schema = Schema::Wrap(GetChromeSchemaData());
+    auto handler =
+        std::make_unique<WebUsbAllowDevicesForUrlsPolicyHandler>(chrome_schema);
+    handler_ = handler.get();
+    handler_list_.AddHandler(std::move(handler));
+  }
+
+  WebUsbAllowDevicesForUrlsPolicyHandler* handler_;
+};
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest, CheckPolicySettings) {
+  PolicyMap policy;
+  PolicyErrorMap errors;
+
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kValidPolicy), nullptr);
+  ASSERT_TRUE(errors.empty());
+  EXPECT_TRUE(handler()->CheckPolicySettings(policy, &errors));
+  EXPECT_TRUE(errors.empty());
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       CheckPolicySettingsWithInvalidTopLevelEntry) {
+  PolicyMap policy;
+  PolicyErrorMap errors;
+
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyInvalidTopLevelEntry), nullptr);
+
+  ASSERT_TRUE(errors.empty());
+  EXPECT_FALSE(handler()->CheckPolicySettings(policy, &errors));
+  EXPECT_FALSE(errors.empty());
+  ASSERT_EQ(errors.size(), 1ul);
+
+  const base::string16 kExpected = base::ASCIIToUTF16(
+      "Schema validation error at \"items[1]\": Missing or invalid required "
+      "property: devices");
+  EXPECT_EQ(errors.GetErrors(key::kWebUsbAllowDevicesForUrls), kExpected);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       CheckPolicySettingsWithMissingDevicesProperty) {
+  PolicyMap policy;
+  PolicyErrorMap errors;
+
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyMissingDevicesProperty), nullptr);
+
+  ASSERT_TRUE(errors.empty());
+  EXPECT_FALSE(handler()->CheckPolicySettings(policy, &errors));
+  EXPECT_FALSE(errors.empty());
+  ASSERT_EQ(errors.size(), 1ul);
+
+  const base::string16 kExpected = base::ASCIIToUTF16(
+      "Schema validation error at \"items[0]\": Missing or invalid required "
+      "property: devices");
+  EXPECT_EQ(errors.GetErrors(key::kWebUsbAllowDevicesForUrls), kExpected);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       CheckPolicySettingsWithMissingUrlPatternsProperty) {
+  PolicyMap policy;
+  PolicyErrorMap errors;
+
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyMissingUrlPatternsProperty),
+      nullptr);
+
+  ASSERT_TRUE(errors.empty());
+  EXPECT_FALSE(handler()->CheckPolicySettings(policy, &errors));
+  EXPECT_FALSE(errors.empty());
+  ASSERT_EQ(errors.size(), 1ul);
+
+  const base::string16 kExpected = base::ASCIIToUTF16(
+      "Schema validation error at \"items[0]\": Missing or invalid required "
+      "property: url_patterns");
+  EXPECT_EQ(errors.GetErrors(key::kWebUsbAllowDevicesForUrls), kExpected);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       CheckPolicySettingsUnknownProperty) {
+  PolicyMap policy;
+  PolicyErrorMap errors;
+
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyUnknownProperty), nullptr);
+
+  ASSERT_TRUE(errors.empty());
+  EXPECT_FALSE(handler()->CheckPolicySettings(policy, &errors));
+  EXPECT_FALSE(errors.empty());
+  ASSERT_EQ(errors.size(), 1ul);
+
+  const base::string16 kExpected = base::ASCIIToUTF16(
+      "Schema validation error at \"items[0].devices.items[0]\": Unknown "
+      "property: serialNumber");
+  EXPECT_EQ(errors.GetErrors(key::kWebUsbAllowDevicesForUrls), kExpected);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       CheckPolicySettingsWithMismatchedVendorIdType) {
+  PolicyMap policy;
+  PolicyErrorMap errors;
+
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyMismatchedVendorIdType), nullptr);
+
+  ASSERT_TRUE(errors.empty());
+  EXPECT_FALSE(handler()->CheckPolicySettings(policy, &errors));
+  EXPECT_FALSE(errors.empty());
+  ASSERT_EQ(errors.size(), 1ul);
+
+  const base::string16 kExpected = base::ASCIIToUTF16(
+      "Schema validation error at \"items[0].devices.items[0]\": The vendor_id "
+      "must be an unsigned short integer");
+  EXPECT_EQ(errors.GetErrors(key::kWebUsbAllowDevicesForUrls), kExpected);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       CheckPolicySettingsWithMismatchedProductIdType) {
+  PolicyMap policy;
+  PolicyErrorMap errors;
+
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyMismatchedProductIdType), nullptr);
+
+  ASSERT_TRUE(errors.empty());
+  EXPECT_FALSE(handler()->CheckPolicySettings(policy, &errors));
+  EXPECT_FALSE(errors.empty());
+  ASSERT_EQ(errors.size(), 1ul);
+
+  const base::string16 kExpected = base::ASCIIToUTF16(
+      "Schema validation error at \"items[0].devices.items[0]\": The "
+      "product_id must be an unsigned short integer");
+  EXPECT_EQ(errors.GetErrors(key::kWebUsbAllowDevicesForUrls), kExpected);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       CheckPolicySettingsWithProductIdWithoutVendorId) {
+  PolicyMap policy;
+  PolicyErrorMap errors;
+
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyProductIdWithoutVendorId), nullptr);
+
+  ASSERT_TRUE(errors.empty());
+  EXPECT_FALSE(handler()->CheckPolicySettings(policy, &errors));
+  EXPECT_FALSE(errors.empty());
+  ASSERT_EQ(errors.size(), 1ul);
+
+  const base::string16 kExpected = base::ASCIIToUTF16(
+      "Schema validation error at \"items[0].devices.items[0]\": A vendor_id "
+      "must also be specified");
+  EXPECT_EQ(errors.GetErrors(key::kWebUsbAllowDevicesForUrls), kExpected);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest, ApplyPolicySettings) {
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, nullptr));
+
+  PolicyMap policy;
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kValidPolicy), nullptr);
+  UpdateProviderPolicy(policy);
+
+  const base::Value* pref_value = nullptr;
+  EXPECT_TRUE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, &pref_value));
+  ASSERT_TRUE(pref_value);
+  ASSERT_TRUE(pref_value->is_list());
+
+  // Ensure that the kManagedWebUsbAllowDevicesForUrls pref is set correctly.
+  const base::Value::ListStorage& list = pref_value->GetList();
+  ASSERT_EQ(list.size(), 2ul);
+
+  // Check the first item's devices list.
+  const base::Value* devices = list[0].FindKey(kDevicesKey);
+  ASSERT_TRUE(devices);
+
+  const base::Value::ListStorage& first_devices_list = devices->GetList();
+  ASSERT_EQ(first_devices_list.size(), 2ul);
+
+  const base::Value* vendor_id = first_devices_list[0].FindKey(kVendorIdKey);
+  ASSERT_TRUE(vendor_id);
+  EXPECT_EQ(vendor_id->GetInt(), 1234);
+
+  const base::Value* product_id = first_devices_list[0].FindKey(kProductIdKey);
+  ASSERT_TRUE(product_id);
+  EXPECT_EQ(product_id->GetInt(), 5678);
+
+  vendor_id = first_devices_list[1].FindKey(kVendorIdKey);
+  ASSERT_TRUE(vendor_id);
+  EXPECT_EQ(vendor_id->GetInt(), 4321);
+
+  product_id = first_devices_list[1].FindKey(kProductIdKey);
+  EXPECT_FALSE(product_id);
+
+  // Check the first item's url_patterns list.
+  const base::Value* url_patterns = list[0].FindKey(kUrlPatternsKey);
+  ASSERT_TRUE(url_patterns);
+
+  const base::Value::ListStorage& first_url_patterns_list =
+      url_patterns->GetList();
+  ASSERT_EQ(first_url_patterns_list.size(), 2ul);
+  ASSERT_TRUE(first_url_patterns_list[0].is_string());
+  ASSERT_TRUE(first_url_patterns_list[1].is_string());
+  EXPECT_EQ(first_url_patterns_list[0].GetString(), "[*.]google.com");
+  EXPECT_EQ(first_url_patterns_list[1].GetString(), "youtube.com");
+
+  // Check the second item's devices list.
+  devices = list[1].FindKey(kDevicesKey);
+  ASSERT_TRUE(devices);
+
+  const base::Value::ListStorage& second_devices_list = devices->GetList();
+  ASSERT_EQ(second_devices_list.size(), 1ul);
+
+  vendor_id = second_devices_list[0].FindKey(kVendorIdKey);
+  EXPECT_FALSE(vendor_id);
+
+  product_id = second_devices_list[0].FindKey(kProductIdKey);
+  EXPECT_FALSE(product_id);
+
+  // Check the second item's url_patterns list.
+  url_patterns = list[1].FindKey(kUrlPatternsKey);
+  ASSERT_TRUE(url_patterns);
+
+  const base::Value::ListStorage& second_url_patterns_list =
+      url_patterns->GetList();
+  ASSERT_EQ(second_url_patterns_list.size(), 1ul);
+  ASSERT_TRUE(second_url_patterns_list[0].is_string());
+  EXPECT_EQ(second_url_patterns_list[0].GetString(), "[*.]crbug.com");
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       ApplyPolicySettingsWithInvalidTopLevelEntry) {
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, nullptr));
+
+  PolicyMap policy;
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyInvalidTopLevelEntry), nullptr);
+  UpdateProviderPolicy(policy);
+
+  const base::Value* pref_value = nullptr;
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, &pref_value));
+  EXPECT_FALSE(pref_value);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       ApplyPolicySettingsWithMissingDevicesProperty) {
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, nullptr));
+
+  PolicyMap policy;
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyMissingDevicesProperty), nullptr);
+  UpdateProviderPolicy(policy);
+  const base::Value* pref_value = nullptr;
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, &pref_value));
+  EXPECT_FALSE(pref_value);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       ApplyPolicySettingsWithMissingUrlPatternsProperty) {
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, nullptr));
+
+  PolicyMap policy;
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyMissingUrlPatternsProperty),
+      nullptr);
+  UpdateProviderPolicy(policy);
+  const base::Value* pref_value = nullptr;
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, &pref_value));
+  EXPECT_FALSE(pref_value);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       ApplyPolicySettingsWithUnknownProperty) {
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, nullptr));
+
+  PolicyMap policy;
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyUnknownProperty), nullptr);
+  UpdateProviderPolicy(policy);
+  const base::Value* pref_value = nullptr;
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, &pref_value));
+  EXPECT_FALSE(pref_value);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       ApplyPolicySettingsWithMismatchedVendorIdType) {
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, nullptr));
+
+  PolicyMap policy;
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyMismatchedVendorIdType), nullptr);
+  UpdateProviderPolicy(policy);
+  const base::Value* pref_value = nullptr;
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, &pref_value));
+  EXPECT_FALSE(pref_value);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       ApplyPolicySettingsWithMismatchedProductIdType) {
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, nullptr));
+
+  PolicyMap policy;
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyMismatchedProductIdType), nullptr);
+  UpdateProviderPolicy(policy);
+  const base::Value* pref_value = nullptr;
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, &pref_value));
+  EXPECT_FALSE(pref_value);
+}
+
+TEST_F(WebUsbAllowDevicesForUrlsPolicyHandlerTest,
+       ApplyPolicySettingsProductIdWithoutVendorId) {
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, nullptr));
+
+  PolicyMap policy;
+  policy.Set(
+      key::kWebUsbAllowDevicesForUrls, PolicyLevel::POLICY_LEVEL_MANDATORY,
+      PolicyScope::POLICY_SCOPE_MACHINE, PolicySource::POLICY_SOURCE_CLOUD,
+      base::JSONReader::Read(kInvalidPolicyProductIdWithoutVendorId), nullptr);
+  UpdateProviderPolicy(policy);
+  const base::Value* pref_value = nullptr;
+  EXPECT_FALSE(
+      store_->GetValue(prefs::kManagedWebUsbAllowDevicesForUrls, &pref_value));
+  EXPECT_FALSE(pref_value);
+}
+
+}  // namespace policy
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 143ea93..ddb534d 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -2586,6 +2586,7 @@
     "../browser/policy/javascript_policy_handler_unittest.cc",
     "../browser/policy/managed_bookmarks_policy_handler_unittest.cc",
     "../browser/policy/profile_policy_connector_unittest.cc",
+    "../browser/policy/webusb_allow_devices_for_urls_policy_handler_unittest.cc",
     "../browser/predictors/autocomplete_action_predictor_table_unittest.cc",
     "../browser/predictors/autocomplete_action_predictor_unittest.cc",
     "../browser/predictors/loading_data_collector_unittest.cc",
diff --git a/chrome/test/data/policy/policy_test_cases.json b/chrome/test/data/policy/policy_test_cases.json
index 202c069b..cf44fa1 100644
--- a/chrome/test/data/policy/policy_test_cases.json
+++ b/chrome/test/data/policy/policy_test_cases.json
@@ -1884,6 +1884,21 @@
     "note": "TODO(bartfab): Flag this with can_be_recommended when http://crbug.com/106682 is fixed."
   },
 
+  "WebUsbAllowDevicesForUrls": {
+    "os": ["win", "linux", "mac", "chromeos", "android"],
+    "test_policy": {
+      "WebUsbAllowDevicesForUrls": [
+        {
+          "devices": [{"vendor_id": 1234, "product_id": 5678}],
+          "url_patterns": ["[*.]google.com", "[*.]youtube.com"]
+        }
+      ]
+    },
+    "pref_mappings": [
+      { "pref": "profile.managed_web_usb_allow_devices_for_urls" }
+    ]
+  },
+
   "WebUsbAskForUrls": {
     "os": ["win", "linux", "mac", "chromeos", "android"],
     "test_policy": { "WebUsbAskForUrls": ["[*.]google.com"] },
diff --git a/components/content_settings/core/browser/content_settings_policy_provider.cc b/components/content_settings/core/browser/content_settings_policy_provider.cc
index fa28ae3d..c8f4b63 100644
--- a/components/content_settings/core/browser/content_settings_policy_provider.cc
+++ b/components/content_settings/core/browser/content_settings_policy_provider.cc
@@ -117,6 +117,7 @@
   registry->RegisterListPref(prefs::kManagedPluginsBlockedForUrls);
   registry->RegisterListPref(prefs::kManagedPopupsAllowedForUrls);
   registry->RegisterListPref(prefs::kManagedPopupsBlockedForUrls);
+  registry->RegisterListPref(prefs::kManagedWebUsbAllowDevicesForUrls);
   registry->RegisterListPref(prefs::kManagedWebUsbAskForUrls);
   registry->RegisterListPref(prefs::kManagedWebUsbBlockedForUrls);
   // Preferences for default content setting policies. If a policy is not set of
diff --git a/components/content_settings/core/common/pref_names.cc b/components/content_settings/core/common/pref_names.cc
index ed5f931..9e18100 100644
--- a/components/content_settings/core/common/pref_names.cc
+++ b/components/content_settings/core/common/pref_names.cc
@@ -73,6 +73,8 @@
     "profile.managed_popups_allowed_for_urls";
 const char kManagedPopupsBlockedForUrls[] =
     "profile.managed_popups_blocked_for_urls";
+const char kManagedWebUsbAllowDevicesForUrls[] =
+    "profile.managed_web_usb_allow_devices_for_urls";
 const char kManagedWebUsbAskForUrls[] = "profile.managed_web_usb_ask_for_urls";
 const char kManagedWebUsbBlockedForUrls[] =
     "profile.managed_web_usb_blocked_for_urls";
diff --git a/components/content_settings/core/common/pref_names.h b/components/content_settings/core/common/pref_names.h
index af5e4db..4ce1bf8 100644
--- a/components/content_settings/core/common/pref_names.h
+++ b/components/content_settings/core/common/pref_names.h
@@ -43,6 +43,7 @@
 extern const char kManagedNotificationsAllowedForUrls[];
 extern const char kManagedNotificationsBlockedForUrls[];
 extern const char kManagedAutoSelectCertificateForUrls[];
+extern const char kManagedWebUsbAllowDevicesForUrls[];
 extern const char kManagedWebUsbAskForUrls[];
 extern const char kManagedWebUsbBlockedForUrls[];
 
diff --git a/components/policy/resources/policy_templates.json b/components/policy/resources/policy_templates.json
index d3ce79a..2239e214c 100644
--- a/components/policy/resources/policy_templates.json
+++ b/components/policy/resources/policy_templates.json
@@ -485,6 +485,7 @@
         'PopupsBlockedForUrls',
         'NotificationsAllowedForUrls',
         'NotificationsBlockedForUrls',
+        "WebUsbAllowDevicesForUrls",
         'WebUsbAskForUrls',
         'WebUsbBlockedForUrls',
       ],
@@ -4189,6 +4190,50 @@
           If this policy is left not set, '3' will be used, and the user will be able to change it.''',
     },
     {
+      'name': 'WebUsbAllowDevicesForUrls',
+      'type': 'dict',
+      'schema': {
+        'type': 'array',
+        'items': {
+          'type': 'object',
+          'properties': {
+            'devices': {
+              'type': 'array',
+              'items': {
+                'type': 'object',
+                'properties': {
+                  'vendor_id': { 'type': 'integer' },
+                  'product_id': { 'type': 'integer' }
+                }
+              }
+            },
+            'url_patterns': {
+              'type': 'array',
+              'items': { 'type': 'string' }
+            }
+          },
+          'required': ['devices', 'url_patterns']
+        }
+      },
+      'supported_on': ['chrome_os:71-', 'android:71-', 'chrome.*:71-'],
+      'features': {
+        'dynamic_refresh': True,
+        'per_profile': True,
+      },
+      'example_value': [{
+        'devices': [{'vendor_id': 1234, 'product_id': 5678}],
+        'url_patterns': ['https://[*].google.com'],
+      }],
+      'id': 486,
+      'caption': '''Automatically grant permission to these sites to connect to USB devices with the given vendor and product IDs.''',
+      'tags': ['website-sharing'],
+      'desc': '''Allows you to set a list of url patterns that specify which sites will automatically be granted permission to access a USB device with the given vendor and product IDs. Each item in the list must contain both devices and url patterns in order for the policy to be valid. Each item in devices can contain a vendor ID and product ID field. Any ID that is omitted is treated as a wildcard with one exception, and that exception is that a product ID cannot be specified without a vendor ID also being specified. Otherwise, the policy will not be valid. Invalid policy values are ignored.
+
+        If this policy is left not set, the global default value will be used for all sites either from the 'DefaultWebUsbGuardSetting' policy if it is set, or the user's personal configuration otherwise.
+
+        URL patterns in this policy should not clash with the ones configured via WebUsbBlockedForUrls. If there is a clash, this policy will take precedence over WebUsbBlockedForUrls and WebUsbAskForUrls.''',
+    },
+    {
       'name': 'WebUsbAskForUrls',
       'type': 'list',
       'schema': {
@@ -13472,5 +13517,5 @@
   },
   'placeholders': [],
   'deleted_policy_ids': [412],
-  'highest_id_currently_used': 485
+  'highest_id_currently_used': 486
 }
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index 42e402d..4b1d16f 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -14609,6 +14609,7 @@
   <int value="483" label="EnterpriseHardwarePlatformAPIEnabled"/>
   <int value="484" label="ReportCrostiniUsageEnabled"/>
   <int value="485" label="VpnConfigAllowed"/>
+  <int value="486" label="WebUsbAllowDevicesForUrls"/>
 </enum>
 
 <enum name="EnterprisePolicyInvalidations">