[go: nahoru, domu]

Consent auditor prototype

This is a prototype of the consent auditor component.

This CL only lays out the component and adds the functionality to
record local consent plus tests.

The functionality to record consents in the user's Google account,
and any callsites, will come in subsequent CLs.

See the design doc for background:
https://docs.google.com/document/d/1Iu040VPbAPdulCpvbnOu1HLh0nXNJfuNMAEckdwmHWQ/

Bug: 781765
Change-Id: I1aef65249afe98c20db7aa692a7c3a007069e56b
Reviewed-on: https://chromium-review.googlesource.com/731423
Commit-Queue: Martin Šrámek <msramek@chromium.org>
Reviewed-by: Jochen Eisinger <jochen@chromium.org>
Reviewed-by: Sylvain Defresne <sdefresne@chromium.org>
Reviewed-by: Bernhard Bauer <bauerb@chromium.org>
Reviewed-by: Christian Dullweber <dullweber@chromium.org>
Reviewed-by: Sky Malice <skym@chromium.org>
Cr-Commit-Position: refs/heads/master@{#515929}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index fd04c90b..e45c997 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -310,6 +310,8 @@
     "conflicts/msi_util_win.h",
     "conflicts/third_party_metrics_recorder_win.cc",
     "conflicts/third_party_metrics_recorder_win.h",
+    "consent_auditor/consent_auditor_factory.cc",
+    "consent_auditor/consent_auditor_factory.h",
     "content_settings/chrome_content_settings_utils.cc",
     "content_settings/chrome_content_settings_utils.h",
     "content_settings/cookie_settings_factory.cc",
@@ -1582,6 +1584,7 @@
     "//components/cloud_devices/common",
     "//components/component_updater",
     "//components/component_updater:crl_set_remover",
+    "//components/consent_auditor",
     "//components/content_settings/core/browser",
     "//components/content_settings/core/common",
     "//components/contextual_search:browser",
diff --git a/chrome/browser/consent_auditor/OWNERS b/chrome/browser/consent_auditor/OWNERS
new file mode 100644
index 0000000..8a99c77
--- /dev/null
+++ b/chrome/browser/consent_auditor/OWNERS
@@ -0,0 +1,4 @@
+dullweber@chromium.org
+msramek@chromium.org
+
+# COMPONENT: Privacy
diff --git a/chrome/browser/consent_auditor/consent_auditor_factory.cc b/chrome/browser/consent_auditor/consent_auditor_factory.cc
new file mode 100644
index 0000000..2bd3592e
--- /dev/null
+++ b/chrome/browser/consent_auditor/consent_auditor_factory.cc
@@ -0,0 +1,55 @@
+// Copyright 2017 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/consent_auditor/consent_auditor_factory.h"
+
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/sync/user_event_service_factory.h"
+#include "components/consent_auditor/consent_auditor.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/prefs/pref_service.h"
+#include "components/version_info/version_info.h"
+
+// static
+ConsentAuditorFactory* ConsentAuditorFactory::GetInstance() {
+  CR_DEFINE_STATIC_LOCAL(ConsentAuditorFactory, factory, ());
+  return &factory;
+}
+
+// static
+consent_auditor::ConsentAuditor* ConsentAuditorFactory::GetForProfile(
+    Profile* profile) {
+  // Recording local consents in Incognito is not useful, as the record would
+  // soon disappear. Consents tied to the user's Google account should retrieve
+  // account information from the original profile. In both cases, there is no
+  // reason to support Incognito.
+  DCHECK(!profile->IsOffTheRecord());
+  return static_cast<consent_auditor::ConsentAuditor*>(
+      GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+ConsentAuditorFactory::ConsentAuditorFactory()
+    : BrowserContextKeyedServiceFactory(
+          "ConsentAuditor",
+          BrowserContextDependencyManager::GetInstance()) {
+  DependsOn(browser_sync::UserEventServiceFactory::GetInstance());
+}
+
+ConsentAuditorFactory::~ConsentAuditorFactory() {}
+
+KeyedService* ConsentAuditorFactory::BuildServiceInstanceFor(
+    content::BrowserContext* context) const {
+  Profile* profile = static_cast<Profile*>(context);
+  return new consent_auditor::ConsentAuditor(
+      profile->GetPrefs(),
+      browser_sync::UserEventServiceFactory::GetForProfile(profile),
+      version_info::GetVersionNumber());
+}
+
+// static
+void ConsentAuditorFactory::RegisterProfilePrefs(
+    user_prefs::PrefRegistrySyncable* registry) {
+  consent_auditor::ConsentAuditor::RegisterProfilePrefs(registry);
+}
diff --git a/chrome/browser/consent_auditor/consent_auditor_factory.h b/chrome/browser/consent_auditor/consent_auditor_factory.h
new file mode 100644
index 0000000..7cca1d0
--- /dev/null
+++ b/chrome/browser/consent_auditor/consent_auditor_factory.h
@@ -0,0 +1,42 @@
+// Copyright 2017 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_CONSENT_AUDITOR_CONSENT_AUDITOR_FACTORY_H_
+#define CHROME_BROWSER_CONSENT_AUDITOR_CONSENT_AUDITOR_FACTORY_H_
+
+#include "base/macros.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace consent_auditor {
+class ConsentAuditor;
+}
+
+namespace user_prefs {
+class PrefRegistrySyncable;
+}
+
+class Profile;
+
+class ConsentAuditorFactory : public BrowserContextKeyedServiceFactory {
+ public:
+  // Returns the singleton instance of ChromeConsentAuditorFactory.
+  static ConsentAuditorFactory* GetInstance();
+
+  // Returns the ContextAuditor associated with |profile|.
+  static consent_auditor::ConsentAuditor* GetForProfile(Profile* profile);
+
+ private:
+  ConsentAuditorFactory();
+  ~ConsentAuditorFactory() override;
+
+  // BrowserContextKeyedServiceFactory:
+  KeyedService* BuildServiceInstanceFor(
+      content::BrowserContext* context) const override;
+  void RegisterProfilePrefs(
+      user_prefs::PrefRegistrySyncable* registry) override;
+
+  DISALLOW_COPY_AND_ASSIGN(ConsentAuditorFactory);
+};
+
+#endif  // CHROME_BROWSER_CONSENT_AUDITOR_CONSENT_AUDITOR_FACTORY_H_
diff --git a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
index 1b1f19c..0980f93 100644
--- a/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
+++ b/chrome/browser/profiles/chrome_browser_main_extra_parts_profiles.cc
@@ -15,6 +15,7 @@
 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
 #include "chrome/browser/browsing_data/chrome_browsing_data_remover_delegate_factory.h"
 #include "chrome/browser/chrome_browser_main.h"
+#include "chrome/browser/consent_auditor/consent_auditor_factory.h"
 #include "chrome/browser/content_settings/cookie_settings_factory.h"
 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/custom_handlers/protocol_handler_registry_factory.h"
@@ -224,6 +225,7 @@
 #if BUILDFLAG(ENABLE_PRINT_PREVIEW)
   CloudPrintProxyServiceFactory::GetInstance();
 #endif
+  ConsentAuditorFactory::GetInstance();
   CookieSettingsFactory::GetInstance();
   NotifierStateTrackerFactory::GetInstance();
   data_use_measurement::ChromeDataUseAscriberServiceFactory::GetInstance();
diff --git a/components/BUILD.gn b/components/BUILD.gn
index 1658504..2b3a34c 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -72,6 +72,7 @@
     "//components/client_update_protocol:unit_tests",
     "//components/cloud_devices/common:unit_tests",
     "//components/component_updater:unit_tests",
+    "//components/consent_auditor:unit_tests",
     "//components/content_settings/core/browser:unit_tests",
     "//components/content_settings/core/common:unit_tests",
     "//components/crx_file:unit_tests",
diff --git a/components/consent_auditor/BUILD.gn b/components/consent_auditor/BUILD.gn
new file mode 100644
index 0000000..bd2ecc4f
--- /dev/null
+++ b/components/consent_auditor/BUILD.gn
@@ -0,0 +1,32 @@
+# Copyright 2017 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.
+
+static_library("consent_auditor") {
+  sources = [
+    "consent_auditor.cc",
+    "consent_auditor.h",
+    "pref_names.cc",
+    "pref_names.h",
+  ]
+
+  deps = [
+    "//components/keyed_service/core",
+    "//components/prefs",
+    "//components/sync",
+  ]
+}
+
+source_set("unit_tests") {
+  testonly = true
+  sources = [
+    "consent_auditor_unittest.cc",
+  ]
+
+  deps = [
+    ":consent_auditor",
+    "//components/prefs:test_support",
+    "//components/sync",
+    "//testing/gtest",
+  ]
+}
diff --git a/components/consent_auditor/DEPS b/components/consent_auditor/DEPS
new file mode 100644
index 0000000..e85ccff
--- /dev/null
+++ b/components/consent_auditor/DEPS
@@ -0,0 +1,5 @@
+include_rules = [
+  "+components/keyed_service",
+  "+components/prefs",
+  "+components/sync",
+]
diff --git a/components/consent_auditor/OWNERS b/components/consent_auditor/OWNERS
new file mode 100644
index 0000000..8a99c77
--- /dev/null
+++ b/components/consent_auditor/OWNERS
@@ -0,0 +1,4 @@
+dullweber@chromium.org
+msramek@chromium.org
+
+# COMPONENT: Privacy
diff --git a/components/consent_auditor/README.md b/components/consent_auditor/README.md
new file mode 100644
index 0000000..2016e67c
--- /dev/null
+++ b/components/consent_auditor/README.md
@@ -0,0 +1,20 @@
+# Consent Auditor
+
+The consent auditor component is a service containing methods used for
+recording and retrieving the records of the exact language the user consented to
+when enabling a feature.
+
+These methods should be called from any UI which enables a feature based on
+the user's consent, and record the consent language that the user has seen.
+If we later find out there was a mistranslation, need to expand the scope of
+a setting etc., we will know which users are affected.
+
+TO EMPHASIZE, these methods should ALWAYS be called from the corresponding UI,
+not from the backend code or pref change observers. The latter could cause
+the consent to be wrongfully recorded if feature was enabled through other
+means (through extensions, a new UI surface added, or even a bug).
+
+Consents related to local features will be stored in a local preference.
+
+Consents related to features for signed-in users will be stored in the user's
+Google account.
diff --git a/components/consent_auditor/consent_auditor.cc b/components/consent_auditor/consent_auditor.cc
new file mode 100644
index 0000000..14df7f3
--- /dev/null
+++ b/components/consent_auditor/consent_auditor.cc
@@ -0,0 +1,66 @@
+// Copyright 2017 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 "components/consent_auditor/consent_auditor.h"
+
+#include <memory>
+
+#include "base/memory/ptr_util.h"
+#include "base/values.h"
+#include "components/consent_auditor/pref_names.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/pref_service.h"
+#include "components/prefs/scoped_user_pref_update.h"
+#include "components/sync/user_events/user_event_service.h"
+
+namespace consent_auditor {
+
+namespace {
+
+const char kLocalConsentDescriptionKey[] = "description";
+const char kLocalConsentConfirmationKey[] = "confirmation";
+const char kLocalConsentVersionInfoKey[] = "version";
+
+}  // namespace
+
+ConsentAuditor::ConsentAuditor(PrefService* pref_service,
+                               syncer::UserEventService* user_event_service,
+                               const std::string& product_version)
+    : pref_service_(pref_service),
+      user_event_service_(user_event_service),
+      product_version_(product_version) {
+  DCHECK(pref_service_);
+  DCHECK(user_event_service_);
+}
+
+ConsentAuditor::~ConsentAuditor() {}
+
+void ConsentAuditor::Shutdown() {
+  user_event_service_ = nullptr;
+}
+
+// static
+void ConsentAuditor::RegisterProfilePrefs(PrefRegistrySimple* registry) {
+  registry->RegisterDictionaryPref(prefs::kLocalConsentsDictionary);
+}
+
+void ConsentAuditor::RecordLocalConsent(const std::string& feature,
+                                        const std::string& description_text,
+                                        const std::string& confirmation_text) {
+  DictionaryPrefUpdate consents_update(pref_service_,
+                                       prefs::kLocalConsentsDictionary);
+  base::DictionaryValue* consents = consents_update.Get();
+  DCHECK(consents);
+
+  base::DictionaryValue record;
+  record.SetKey(kLocalConsentDescriptionKey, base::Value(description_text));
+  record.SetKey(kLocalConsentConfirmationKey, base::Value(confirmation_text));
+  record.SetKey(kLocalConsentVersionInfoKey, base::Value(product_version_));
+
+  // TODO(msramek): Record the user language. crbug.com/781765
+
+  consents->SetKey(feature, std::move(record));
+}
+
+}  // namespace consent_auditor
diff --git a/components/consent_auditor/consent_auditor.h b/components/consent_auditor/consent_auditor.h
new file mode 100644
index 0000000..b04ecad
--- /dev/null
+++ b/components/consent_auditor/consent_auditor.h
@@ -0,0 +1,53 @@
+// Copyright 2017 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 COMPONENTS_CONSENT_AUDITOR_CONSENT_AUDITOR_H_
+#define COMPONENTS_CONSENT_AUDITOR_CONSENT_AUDITOR_H_
+
+#include <string>
+
+#include "base/macros.h"
+#include "components/keyed_service/core/keyed_service.h"
+
+namespace syncer {
+class UserEventService;
+}
+
+class PrefService;
+class PrefRegistrySimple;
+
+namespace consent_auditor {
+
+class ConsentAuditor : public KeyedService {
+ public:
+  ConsentAuditor(PrefService* pref_service,
+                 syncer::UserEventService* user_event_service,
+                 const std::string& product_version);
+  ~ConsentAuditor() override;
+
+  // KeyedService:
+  void Shutdown() override;
+
+  // Registers the preferences needed by this service.
+  static void RegisterProfilePrefs(PrefRegistrySimple* registry);
+
+  // Records that the user consented to a |feature|. The user was presented with
+  // |description_text| and accepted it by interacting |confirmation_text|
+  // (e.g. clicking on a button; empty if not applicable).
+  // Returns true if successful.
+  void RecordLocalConsent(const std::string& feature,
+                          const std::string& description_text,
+                          const std::string& confirmation_text);
+
+ private:
+  PrefService* pref_service_;
+  syncer::UserEventService* user_event_service_;
+  std::string product_version_;
+
+  DISALLOW_COPY_AND_ASSIGN(ConsentAuditor);
+};
+
+}  // namespace consent_auditor
+
+#endif  // COMPONENTS_CONSENT_AUDITOR_CONSENT_AUDITOR_H_
diff --git a/components/consent_auditor/consent_auditor_unittest.cc b/components/consent_auditor/consent_auditor_unittest.cc
new file mode 100644
index 0000000..f5f11518
--- /dev/null
+++ b/components/consent_auditor/consent_auditor_unittest.cc
@@ -0,0 +1,127 @@
+// Copyright 2017 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 "components/consent_auditor/consent_auditor.h"
+
+#include <memory>
+
+#include "components/consent_auditor/pref_names.h"
+#include "components/prefs/testing_pref_service.h"
+#include "components/sync/user_events/fake_user_event_service.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace consent_auditor {
+
+namespace {
+
+const char kLocalConsentDescriptionKey[] = "description";
+const char kLocalConsentConfirmationKey[] = "confirmation";
+const char kLocalConsentVersionInfoKey[] = "version";
+
+// Fake product version for testing.
+const char kCurrentProductVersion[] = "1.2.3.4";
+
+}  // namespace
+
+class ConsentAuditorTest : public testing::Test {
+ public:
+  void SetUp() override {
+    pref_service_ = base::MakeUnique<TestingPrefServiceSimple>();
+    user_event_service_ = base::MakeUnique<syncer::FakeUserEventService>();
+    ConsentAuditor::RegisterProfilePrefs(pref_service_->registry());
+    consent_auditor_ = base::MakeUnique<ConsentAuditor>(
+        pref_service_.get(), user_event_service_.get(), kCurrentProductVersion);
+  }
+
+  void UpdateProductVersion(const std::string& new_product_version) {
+    // We'll have to recreate |consent_auditor| in order to update
+    // a new version. This is not a problem, as in reality we'd have to restart
+    // Chrome to update the version, let alone just recreate this class.
+    consent_auditor_ = base::MakeUnique<ConsentAuditor>(
+        pref_service_.get(), user_event_service_.get(), new_product_version);
+  }
+
+  ConsentAuditor* consent_auditor() { return consent_auditor_.get(); }
+
+  PrefService* pref_service() const { return pref_service_.get(); }
+
+ private:
+  std::unique_ptr<ConsentAuditor> consent_auditor_;
+  std::unique_ptr<TestingPrefServiceSimple> pref_service_;
+  std::unique_ptr<syncer::FakeUserEventService> user_event_service_;
+};
+
+TEST_F(ConsentAuditorTest, LocalConsentPrefRepresentation) {
+  // No consents are written at first.
+  EXPECT_FALSE(pref_service()->HasPrefPath(prefs::kLocalConsentsDictionary));
+
+  // Record a consent and check that it appears in the prefs.
+  const std::string kFeature1Description = "This will enable feature 1.";
+  const std::string kFeature1Confirmation = "OK.";
+  consent_auditor()->RecordLocalConsent("feature1", kFeature1Description,
+                                        kFeature1Confirmation);
+  ASSERT_TRUE(pref_service()->HasPrefPath(prefs::kLocalConsentsDictionary));
+  const base::DictionaryValue* consents =
+      pref_service()->GetDictionary(prefs::kLocalConsentsDictionary);
+  ASSERT_TRUE(consents);
+  const base::Value* record =
+      consents->FindKeyOfType("feature1", base::Value::Type::DICTIONARY);
+  ASSERT_TRUE(record);
+  const base::Value* description = record->FindKey(kLocalConsentDescriptionKey);
+  const base::Value* confirmation =
+      record->FindKey(kLocalConsentConfirmationKey);
+  const base::Value* version = record->FindKey(kLocalConsentVersionInfoKey);
+  ASSERT_TRUE(description);
+  ASSERT_TRUE(confirmation);
+  ASSERT_TRUE(version);
+  EXPECT_EQ(description->GetString(), kFeature1Description);
+  EXPECT_EQ(confirmation->GetString(), kFeature1Confirmation);
+  EXPECT_EQ(version->GetString(), kCurrentProductVersion);
+
+  // Do the same for another feature.
+  const std::string kFeature2Description = "Enable feature 2?";
+  const std::string kFeature2Confirmation = "Yes.";
+  consent_auditor()->RecordLocalConsent("feature2", kFeature2Description,
+                                        kFeature2Confirmation);
+  record = consents->FindKeyOfType("feature2", base::Value::Type::DICTIONARY);
+  ASSERT_TRUE(record);
+  description = record->FindKey(kLocalConsentDescriptionKey);
+  confirmation = record->FindKey(kLocalConsentConfirmationKey);
+  version = record->FindKey(kLocalConsentVersionInfoKey);
+  ASSERT_TRUE(description);
+  ASSERT_TRUE(confirmation);
+  ASSERT_TRUE(version);
+  EXPECT_EQ(description->GetString(), kFeature2Description);
+  EXPECT_EQ(confirmation->GetString(), kFeature2Confirmation);
+  EXPECT_EQ(version->GetString(), kCurrentProductVersion);
+
+  // They are two separate records; the latter did not overwrite the former.
+  EXPECT_EQ(2u, consents->size());
+  EXPECT_TRUE(
+      consents->FindKeyOfType("feature1", base::Value::Type::DICTIONARY));
+
+  // Overwrite an existing consent and use a different product version.
+  const std::string kFeature2NewDescription = "Re-enable feature 2?";
+  const std::string kFeature2NewConfirmation = "Yes again.";
+  const std::string kFeature2NewProductVersion = "5.6.7.8";
+  UpdateProductVersion(kFeature2NewProductVersion);
+  consent_auditor()->RecordLocalConsent("feature2", kFeature2NewDescription,
+                                        kFeature2NewConfirmation);
+  record = consents->FindKeyOfType("feature2", base::Value::Type::DICTIONARY);
+  ASSERT_TRUE(record);
+  description = record->FindKey(kLocalConsentDescriptionKey);
+  confirmation = record->FindKey(kLocalConsentConfirmationKey);
+  version = record->FindKey(kLocalConsentVersionInfoKey);
+  ASSERT_TRUE(description);
+  ASSERT_TRUE(confirmation);
+  ASSERT_TRUE(version);
+  EXPECT_EQ(description->GetString(), kFeature2NewDescription);
+  EXPECT_EQ(confirmation->GetString(), kFeature2NewConfirmation);
+  EXPECT_EQ(version->GetString(), kFeature2NewProductVersion);
+
+  // We still have two records.
+  EXPECT_EQ(2u, consents->size());
+}
+
+}  // namespace consent_auditor
diff --git a/components/consent_auditor/pref_names.cc b/components/consent_auditor/pref_names.cc
new file mode 100644
index 0000000..c040542
--- /dev/null
+++ b/components/consent_auditor/pref_names.cc
@@ -0,0 +1,13 @@
+// Copyright 2017 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 "components/consent_auditor/pref_names.h"
+
+namespace consent_auditor {
+namespace prefs {
+
+const char kLocalConsentsDictionary[] = "local_consents";
+
+}  // namespace prefs
+}  // namespace consent_auditor
diff --git a/components/consent_auditor/pref_names.h b/components/consent_auditor/pref_names.h
new file mode 100644
index 0000000..ef0049a
--- /dev/null
+++ b/components/consent_auditor/pref_names.h
@@ -0,0 +1,11 @@
+// Copyright 2017 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.
+
+namespace consent_auditor {
+namespace prefs {
+
+extern const char kLocalConsentsDictionary[];
+
+}  // namespace prefs
+}  // namespace consent_auditor