libbrillo: policy: Add new update time restrictions policy

Add a new device policy from chrome_device_policy.proto:
DeviceAutoUpdateTimeRestrictions

BUG=chromium:852860
TEST=libpolicy unit tests
CQ-DEPEND=CL:1136538
Change-Id: Iaef8791f683af00668dcd4041282758cc26b8fbf
Reviewed-on: https://chromium-review.googlesource.com/1101707
Commit-Ready: Adolfo Higueros <adokar@google.com>
Tested-by: Adolfo Higueros <adokar@google.com>
Reviewed-by: Dan Erat <derat@chromium.org>
diff --git a/policy/device_policy.h b/policy/device_policy.h
index b71d9de..52e73d5 100644
--- a/policy/device_policy.h
+++ b/policy/device_policy.h
@@ -12,6 +12,7 @@
 #include <vector>
 
 #include <base/macros.h>
+#include <base/time/time.h>
 
 #pragma GCC visibility push(default)
 
@@ -35,6 +36,22 @@
     uint16_t product_id;
   };
 
+  // Time interval represented by two |day_of_week| and |time| pairs. The start
+  // of the interval is inclusive and the end is exclusive. The time represented
+  // by those pairs will be interpreted to be in the local timezone. Because of
+  // this, there exists the possibility of intervals being repeated or skipped
+  // in a day with daylight savings transitions, this is expected behavior.
+  struct WeeklyTimeInterval {
+    // Value is from 1 to 7 (1 = Monday, 2 = Tuesday, etc.). All values outside
+    // this range are invalid and will be discarded.
+    int start_day_of_week;
+    // Time since the start of the day. This value will be interpreted to be in
+    // the system's current timezone when used for range checking.
+    base::TimeDelta start_time;
+    int end_day_of_week;
+    base::TimeDelta end_time;
+  };
+
   DevicePolicy();
   virtual ~DevicePolicy();
 
@@ -178,6 +195,13 @@
   // U2F or U2F_EXTENDED). Returns true on success.
   virtual bool GetSecondFactorAuthenticationMode(int* mode_out) const = 0;
 
+  // Writes the valid time intervals to |intervals_out|. These
+  // intervals are taken from the disallowed time intervals field in the
+  // AutoUpdateSettingsProto. Returns true if the intervals in the proto are
+  // valid.
+  virtual bool GetDisallowedTimeIntervals(
+      std::vector<WeeklyTimeInterval>* intervals_out) const = 0;
+
  private:
   // Verifies that the policy signature is correct.
   virtual bool VerifyPolicySignature() = 0;
diff --git a/policy/device_policy_impl.cc b/policy/device_policy_impl.cc
index 6189ca7..4d506ba 100644
--- a/policy/device_policy_impl.cc
+++ b/policy/device_policy_impl.cc
@@ -8,9 +8,12 @@
 
 #include <base/containers/adapters.h>
 #include <base/files/file_util.h>
+#include <base/json/json_reader.h>
 #include <base/logging.h>
 #include <base/macros.h>
 #include <base/memory/ptr_util.h>
+#include <base/time/time.h>
+#include <base/values.h>
 #include <openssl/evp.h>
 #include <openssl/x509.h>
 
@@ -98,6 +101,50 @@
   return kConnectionTypes[type];
 }
 
+// TODO(adokar): change type to base::Optional<int> when available.
+int ConvertDayOfWeekStringToInt(const std::string& day_of_week_str) {
+  if (day_of_week_str == "Sunday") return 0;
+  if (day_of_week_str == "Monday") return 1;
+  if (day_of_week_str == "Tuesday") return 2;
+  if (day_of_week_str == "Wednesday") return 3;
+  if (day_of_week_str == "Thursday") return 4;
+  if (day_of_week_str == "Friday") return 5;
+  if (day_of_week_str == "Saturday") return 6;
+  return -1;
+}
+
+bool DecodeWeeklyTimeFromValue(const base::DictionaryValue& dict_value,
+                               int* day_of_week_out,
+                               base::TimeDelta* time_out) {
+  std::string day_of_week_str;
+  if (!dict_value.GetString("day_of_week", &day_of_week_str)) {
+    LOG(ERROR) << "Day of the week is absent.";
+    return false;
+  }
+  *day_of_week_out = ConvertDayOfWeekStringToInt(day_of_week_str);
+  if (*day_of_week_out == -1) {
+    LOG(ERROR) << "Undefined day of the week: " << day_of_week_str;
+    return false;
+  }
+
+  int hours;
+  if (!dict_value.GetInteger("hours", &hours) || hours < 0 || hours > 23) {
+    LOG(ERROR) << "Hours are absent or are outside of the range [0, 24).";
+    return false;
+  }
+
+  int minutes;
+  if (!dict_value.GetInteger("minutes", &minutes) || minutes < 0 ||
+      minutes > 59) {
+    LOG(ERROR) << "Minutes are absent or are outside the range [0, 60)";
+    return false;
+  }
+
+  *time_out =
+      base::TimeDelta::FromMinutes(minutes) + base::TimeDelta::FromHours(hours);
+  return true;
+}
+
 }  // namespace
 
 DevicePolicyImpl::DevicePolicyImpl()
@@ -510,6 +557,65 @@
   return true;
 }
 
+bool DevicePolicyImpl::GetDisallowedTimeIntervals(
+    std::vector<WeeklyTimeInterval>* intervals_out) const {
+  if (!device_policy_.has_auto_update_settings()) {
+    return false;
+  }
+
+  const em::AutoUpdateSettingsProto& proto =
+      device_policy_.auto_update_settings();
+
+  if (!proto.has_disallowed_time_intervals()) {
+    return false;
+  }
+
+  // Decode the JSON string
+  std::string error;
+  std::unique_ptr<base::Value> decoded_json =
+      base::JSONReader::ReadAndReturnError(proto.disallowed_time_intervals(),
+                                           base::JSON_ALLOW_TRAILING_COMMAS,
+                                           NULL, &error);
+  if (!decoded_json) {
+    LOG(ERROR) << "Invalid JSON string " << error;
+    return false;
+  }
+
+  std::unique_ptr<base::ListValue> list_val =
+      base::ListValue::From(std::move(decoded_json));
+  if (!list_val) {
+    LOG(ERROR) << "JSON string is not a list";
+    return false;
+  }
+
+  intervals_out->clear();
+
+  for (const auto& interval_value : *list_val) {
+    base::DictionaryValue* interval_dict;
+    if (!interval_value->GetAsDictionary(&interval_dict)) {
+      LOG(ERROR) << "Invalid JSON string given. Interval is not a dict.";
+      return false;
+    }
+    base::DictionaryValue* start;
+    base::DictionaryValue* end;
+    if (!interval_dict->GetDictionary("start", &start) ||
+        !interval_dict->GetDictionary("end", &end)) {
+      LOG(ERROR) << "Interval is missing start/end.";
+      return false;
+    }
+    WeeklyTimeInterval weekly_interval;
+    if (!DecodeWeeklyTimeFromValue(*start, &weekly_interval.start_day_of_week,
+                                   &weekly_interval.start_time) ||
+        !DecodeWeeklyTimeFromValue(*end, &weekly_interval.end_day_of_week,
+                                   &weekly_interval.end_time)) {
+      return false;
+    }
+
+    intervals_out->push_back(weekly_interval);
+  }
+  return true;
+}
+
 bool DevicePolicyImpl::VerifyPolicyFile(const base::FilePath& policy_path) {
   if (!verify_root_ownership_) {
     return true;
diff --git a/policy/device_policy_impl.h b/policy/device_policy_impl.h
index 9ec39ac..ee00f0f 100644
--- a/policy/device_policy_impl.h
+++ b/policy/device_policy_impl.h
@@ -78,6 +78,8 @@
   bool GetAutoLaunchedKioskAppId(std::string* app_id_out) const override;
   bool IsEnterpriseManaged() const override;
   bool GetSecondFactorAuthenticationMode(int* mode_out) const override;
+  bool GetDisallowedTimeIntervals(
+      std::vector<WeeklyTimeInterval>* intervals_out) const override;
 
   // Methods that can be used only for testing.
   void set_policy_data_for_testing(
diff --git a/policy/mock_device_policy.h b/policy/mock_device_policy.h
index eba80f4..2999a18 100644
--- a/policy/mock_device_policy.h
+++ b/policy/mock_device_policy.h
@@ -101,7 +101,8 @@
   MOCK_CONST_METHOD1(GetAutoLaunchedKioskAppId, bool(std::string*));
   MOCK_CONST_METHOD0(IsEnterpriseManaged, bool());
   MOCK_CONST_METHOD1(GetSecondFactorAuthenticationMode, bool(int*));
-
+  MOCK_CONST_METHOD1(GetDisallowedTimeIntervals,
+                     bool(std::vector<WeeklyTimeInterval>*));
   MOCK_METHOD0(VerifyPolicyFiles, bool(void));
   MOCK_METHOD0(VerifyPolicySignature, bool(void));
 };
diff --git a/policy/tests/libpolicy_unittest.cc b/policy/tests/libpolicy_unittest.cc
index 384bd69..6fc7258 100644
--- a/policy/tests/libpolicy_unittest.cc
+++ b/policy/tests/libpolicy_unittest.cc
@@ -178,6 +178,21 @@
   ASSERT_TRUE(policy.GetSecondFactorAuthenticationMode(&int_value));
   EXPECT_EQ(2, int_value);
 
+  std::vector<DevicePolicy::WeeklyTimeInterval> intervals;
+  ASSERT_TRUE(policy.GetDisallowedTimeIntervals(&intervals));
+  ASSERT_EQ(2, intervals.size());
+  EXPECT_EQ(4, intervals[0].start_day_of_week);
+  EXPECT_EQ(base::TimeDelta::FromMinutes(30) + base::TimeDelta::FromHours(12),
+            intervals[0].start_time);
+  EXPECT_EQ(6, intervals[0].end_day_of_week);
+  EXPECT_EQ(base::TimeDelta::FromMinutes(15) + base::TimeDelta::FromHours(3),
+            intervals[0].end_time);
+  EXPECT_EQ(1, intervals[1].start_day_of_week);
+  EXPECT_EQ(base::TimeDelta::FromMinutes(10) + base::TimeDelta::FromHours(20),
+            intervals[1].start_time);
+  EXPECT_EQ(3, intervals[1].end_day_of_week);
+  EXPECT_EQ(base::TimeDelta::FromMinutes(20), intervals[1].end_time);
+
   // Reloading the protobuf should succeed.
   EXPECT_TRUE(provider.Reload());
 }
@@ -207,6 +222,7 @@
   bool bool_value;
   std::string string_value;
   std::vector<DevicePolicy::UsbDeviceId> list_device;
+  std::vector<DevicePolicy::WeeklyTimeInterval> intervals;
 
   EXPECT_FALSE(policy.GetPolicyRefreshRate(&int_value));
   EXPECT_FALSE(policy.GetUserWhitelist(&list_value));
@@ -235,6 +251,7 @@
   EXPECT_FALSE(policy.GetAllowKioskAppControlChromeVersion(&bool_value));
   EXPECT_FALSE(policy.GetUsbDetachableWhitelist(&list_device));
   EXPECT_FALSE(policy.GetSecondFactorAuthenticationMode(&int_value));
+  EXPECT_FALSE(policy.GetDisallowedTimeIntervals(&intervals));
 }
 
 // Verify that the library will correctly recognize and signal missing files.
diff --git a/policy/tests/whitelist/policy_all b/policy/tests/whitelist/policy_all
index 07999f6..c0dfaaf 100644
--- a/policy/tests/whitelist/policy_all
+++ b/policy/tests/whitelist/policy_all
Binary files differ