Add tlBMC Redfish PATCH override route and dev-signed safety gating for sensors

This change implements software sensor override management via Redfish:
- Supports `PATCH /redfish/v1/Chassis/<ChassisId>/Sensors/<SensorId>` with payload `Oem.Google.SensorValueOverridden: true` and `Reading` to instantiate a software override (enforces that `Reading` must be present).
- Supports `PATCH` with `Oem.Google.SensorValueOverridden: false` to clear an active override (enforces that `Reading` must NOT be present).
- Conditionally decorates `GET` responses with `Oem.Google.SensorValueOverridden: true` only when a software override is active, omitting the field entirely when inactive.
- Implements compile-time safety gating using `constexpr bool enableTlbmcSensorOverride` from `bmcweb_config.h` (configured via Meson option `tlbmc-sensor-override` and Bazel build flag `tlbmc_sensor_override`), with runtime environment variable fallback `ENABLE_TLBMC_SENSOR_OVERRIDE` for test environments. Returns HTTP 403 Forbidden (`Base.1.13.0.ActionNotSupported`) on production builds when disabled.

Google-Bug-Id:550410302
PiperOrigin-RevId: 979525293
Change-Id: I1843082e86e8a0e6ff422f3f7a2c92199fdd7abf
diff --git a/config/bmcweb_config.h.in b/config/bmcweb_config.h.in
index 396d7c6..440bbe7 100644
--- a/config/bmcweb_config.h.in
+++ b/config/bmcweb_config.h.in
@@ -44,6 +44,7 @@
 constexpr bool enableFastSanity = @ENABLE_FAST_SANITY@ == 1;
 constexpr const char* entityConfigLocation = "@ENTITY_CONFIG_LOCATION@";
 constexpr bool tlbmcAllowSensorCreationFailure = @TLBMC_ALLOW_SENSOR_CREATION_FAILURE@ == 1;
+constexpr bool enableTlbmcSensorOverride = @ENABLE_TLBMC_SENSOR_OVERRIDE@ == 1;
 constexpr bool enableRedfishSystemBootNumber = @ENABLE_REDFISH_SYSTEM_BOOTNUMBER@ == 1;
 constexpr bool enableConfigureCoreCount = @ENABLE_CONFIGURE_CORE_COUNT@ == 1;
 constexpr bool enableRedfishConfigureIfs = @ENABLE_REDFISH_CONFIGURE_IFS@ == 1;
diff --git a/config/meson.build b/config/meson.build
index dd403a6..16ef745 100644
--- a/config/meson.build
+++ b/config/meson.build
@@ -26,6 +26,7 @@
 conf_data.set10('ENABLE_FAST_SANITY', get_option('fast-sanity').enabled())
 conf_data.set('ENTITY_CONFIG_LOCATION', get_option('entity-config-location'))
 conf_data.set10('TLBMC_ALLOW_SENSOR_CREATION_FAILURE', get_option('tlbmc-allow-sensor-creation-failure').enabled())
+conf_data.set10('ENABLE_TLBMC_SENSOR_OVERRIDE', get_option('tlbmc-sensor-override').enabled())
 conf_data.set10('ENABLE_REDFISH_SYSTEM_BOOTNUMBER', get_option('enable-redfish-system-bootnumber').enabled())
 conf_data.set10('ENABLE_CONFIGURE_CORE_COUNT', get_option('enable-configure-core-count').enabled())
 conf_data.set10('ENABLE_REDFISH_CONFIGURE_IFS', get_option('redfish-configure-ifs').enabled())
diff --git a/meson_options.txt b/meson_options.txt
index bbfdc0a..ad0b3e0 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -847,6 +847,13 @@
 )
 
 option(
+    'tlbmc-sensor-override',
+    type: 'feature',
+    value: 'disabled',
+    description: 'Enable tlBMC sensor error injection and software overrides for tests. Do not enable in production builds'
+)
+
+option(
     'sysfs-base-path-ut',
     type : 'string',
     value : '/tmpfs/src/ci_workspace/openbmc-build-scripts/additional',
diff --git a/tlbmc/redfish/routes/sensor.cc b/tlbmc/redfish/routes/sensor.cc
index a46d9e3..cd15e38 100644
--- a/tlbmc/redfish/routes/sensor.cc
+++ b/tlbmc/redfish/routes/sensor.cc
@@ -9,6 +9,7 @@
 #include <utility>
 #include <vector>
 
+#include "absl/flags/flag.h"
 #include "absl/functional/bind_front.h"
 #include "absl/status/status.h"
 #include "absl/status/statusor.h"
@@ -16,6 +17,7 @@
 #include "absl/strings/str_format.h"
 #include "absl/strings/string_view.h"
 #include "absl/strings/substitute.h"
+#include "bmcweb_config.h"
 #include "http_request.hpp"
 #include "async_resp.hpp"
 #include <nlohmann/json.hpp>
@@ -35,6 +37,10 @@
 #include "tlbmc/store/store.h"
 #include "smart_router.h"
 
+ABSL_FLAG(bool, enable_tlbmc_sensor_override, enableTlbmcSensorOverride,
+          "Enable creation of tlBMC sensor overrides. Defaults to true when "
+          "compiled with tlbmc-sensor-override and false otherwise.");
+
 namespace milotic_tlbmc::sensor {
 
 namespace {
@@ -135,6 +141,120 @@
   FillResponseWithSensorData(sensor, sensor_pointer, resp, store);
 }
 
+void SetSuccessResponse(RedfishResponse& resp, absl::string_view message,
+                        absl::string_view chassis_id,
+                        absl::string_view sensor_id) {
+  nlohmann::json success = {{{"@odata.type", "#Message.v1_1_1.Message"},
+                             {"Message", message},
+                             {"MessageArgs", nlohmann::json::array_t()},
+                             {"MessageId", "Base.1.13.0.Success"},
+                             {"MessageSeverity", "OK"},
+                             {"Resolution", "None"}}};
+  resp.SetKeyInJsonBody("/@Message.ExtendedInfo", success);
+  resp.SetKeyInJsonBody("/@odata.id",
+                        CreateUrl({"redfish", "v1", "Chassis", chassis_id,
+                                   "Sensors", sensor_id}));
+}
+
+const nlohmann::json* FindSensorOverrideControl(const nlohmann::json& body) {
+  auto oem_it = body.find("Oem");
+  if (oem_it == body.end() || !oem_it->is_object()) {
+    return nullptr;
+  }
+  auto google_it = oem_it->find("Google");
+  if (google_it == oem_it->end() || !google_it->is_object()) {
+    return nullptr;
+  }
+  auto override_it = google_it->find("SensorValueOverridden");
+  if (override_it == google_it->end()) {
+    return nullptr;
+  }
+  return &(*override_it);
+}
+
+absl::StatusOr<SensorValue> ParseReadingValue(
+    const nlohmann::json& reading_json, const Sensor& sensor) {
+  SensorValue val;
+  if (sensor.GetSensorAttributesStatic().unit() == UNIT_COUNT) {
+    if (!reading_json.is_number_integer()) {
+      return absl::InvalidArgumentError(
+          "Reading must be an integer for count unit");
+    }
+    val.set_count(reading_json.get<int64_t>());
+  } else {
+    if (!reading_json.is_number()) {
+      return absl::InvalidArgumentError("Reading must be a number");
+    }
+    val.set_reading(reading_json.get<double>());
+  }
+  return val;
+}
+
+// Handles PATCH requests configuring or clearing software sensor overrides.
+// Explicit behavior:
+// 1. If SensorValueOverridden is true, a 'Reading' field MUST be provided.
+// 2. If SensorValueOverridden is false, a 'Reading' field MUST NOT be provided.
+void HandleSensorOverridePatch(Store& store, const Sensor& sensor,
+                               const nlohmann::json& body,
+                               const nlohmann::json& override_val_json,
+                               RedfishResponse& resp,
+                               absl::string_view chassis_id,
+                               absl::string_view sensor_id) {
+  if (!absl::GetFlag(FLAGS_enable_tlbmc_sensor_override)) {
+    resp.SetToForbidden(
+        "Sensor error injection and software overrides are disabled on "
+        "production images. Use a dev-signed image or enable "
+        "--enable_tlbmc_sensor_override.");
+    return;
+  }
+
+  if (!override_val_json.is_boolean()) {
+    resp.SetToBadRequest("SensorValueOverridden must be a boolean");
+    return;
+  }
+
+  bool is_override = override_val_json.get<bool>();
+  auto reading_it = body.find("Reading");
+
+  // Behavior 1: If Override is false, we MUST NOT receive a reading.
+  if (!is_override) {
+    if (reading_it != body.end()) {
+      resp.SetToBadRequest(
+          "Clearing override must not include 'Reading' field");
+      return;
+    }
+    absl::Status status = store.ClearSensorOverride(std::string(sensor_id));
+    if (!status.ok()) {
+      resp.SetToAbslStatus(status);
+      return;
+    }
+    SetSuccessResponse(resp, "Sensor override successfully cleared.",
+                       chassis_id, sensor_id);
+    return;
+  }
+
+  // Behavior 2: If Override is true, we MUST provide a reading.
+  if (reading_it == body.end()) {
+    resp.SetToBadRequest("Setting override requires 'Reading' field");
+    return;
+  }
+
+  absl::StatusOr<SensorValue> val = ParseReadingValue(*reading_it, sensor);
+  if (!val.ok()) {
+    resp.SetToBadRequest(val.status().message());
+    return;
+  }
+
+  absl::Status status = store.SetSensorOverride(std::string(sensor_id), *val);
+  if (!status.ok()) {
+    resp.SetToAbslStatus(status);
+    return;
+  }
+
+  SetSuccessResponse(resp, "Sensor override successfully created.", chassis_id,
+                     sensor_id);
+}
+
 void HandleSensorPatch(const RedfishApp& app, const RedfishRequest& req,
                        RedfishResponse& resp, const std::string& chassis_id,
                        const std::string& sensor_id) {
@@ -154,42 +274,35 @@
     return;
   }
 
-  if (!body.contains("Reading")) {
+  // Check for OEM Google Override control.
+  const nlohmann::json* override_json = FindSensorOverrideControl(body);
+  if (override_json != nullptr) {
+    HandleSensorOverridePatch(store, *sensor, body, *override_json, resp,
+                              chassis_id, sensor_id);
+    return;
+  }
+
+  // Standard sensor write PATCH.
+  auto reading_it = body.find("Reading");
+  if (reading_it == body.end()) {
     resp.SetToBadRequest("Request does not contain 'Reading' field");
     return;
   }
 
-  SensorValue val;
-  if (sensor->GetSensorAttributesStatic().unit() == UNIT_COUNT) {
-    if (!body["Reading"].is_number_integer()) {
-      resp.SetToBadRequest("Reading must be an integer for count unit");
-      return;
-    }
-    val.set_count(body["Reading"].get<int64_t>());
-  } else {
-    if (!body["Reading"].is_number()) {
-      resp.SetToBadRequest("Reading must be a number");
-      return;
-    }
-    val.set_reading(body["Reading"].get<double>());
+  absl::StatusOr<SensorValue> val = ParseReadingValue(*reading_it, *sensor);
+  if (!val.ok()) {
+    resp.SetToBadRequest(val.status().message());
+    return;
   }
 
-  absl::Status status = store.WriteToSensor(sensor_id, val);
+  absl::Status status = store.WriteToSensor(sensor_id, *val);
   if (!status.ok()) {
     resp.SetToAbslStatus(status);
     return;
   }
 
-  nlohmann::json success = {{{"@odata.type", "#Message.v1_1_1.Message"},
-                             {"Message", "The request completed successfully."},
-                             {"MessageArgs", nlohmann::json::array_t()},
-                             {"MessageId", "Base.1.13.0.Success"},
-                             {"MessageSeverity", "OK"},
-                             {"Resolution", "None"}}};
-  resp.SetKeyInJsonBody("/@Message.ExtendedInfo", success);
-  resp.SetKeyInJsonBody("/@odata.id",
-                        CreateUrl({"redfish", "v1", "Chassis", chassis_id,
-                                   "Sensors", sensor_id}));
+  SetSuccessResponse(resp, "The request completed successfully.", chassis_id,
+                     sensor_id);
 }
 
 std::string_view GetOemSensorStateString(Status status) {
@@ -450,6 +563,11 @@
       }
     }
   }
+
+  if (sensor->IsOverridden()) {
+    resp.SetKeyInJsonBody(
+        sensor_pointer / "Oem" / "Google" / "SensorValueOverridden", true);
+  }
 }
 
 void RegisterRoutes(RedfishApp& app) {
diff --git a/tlbmc/redfish/routes/sensor.h b/tlbmc/redfish/routes/sensor.h
index ec6a5f3..cb3f188 100644
--- a/tlbmc/redfish/routes/sensor.h
+++ b/tlbmc/redfish/routes/sensor.h
@@ -3,12 +3,15 @@
 
 #include <memory>
 
+#include "absl/flags/declare.h"
 #include <nlohmann/json.hpp>
 #include "tlbmc/redfish/app.h"
 #include "tlbmc/redfish/response.h"
 #include "tlbmc/sensors/sensor.h"
 #include "tlbmc/store/store.h"
 
+ABSL_DECLARE_FLAG(bool, enable_tlbmc_sensor_override);
+
 namespace milotic_tlbmc::sensor {
 
 // Fills the response as a Redfish Sensor resource type with the sensor data.