fix: answer 400 for bad role updates and stop time-range overflow and PromQL time panics (#16190)

This commit is contained in:
Shrinath Rao
2026-11-02 07:56:09 +00:00
committed by GitHub
parent d2289786a4
commit 3f3388e04b
21 changed files with 246 additions and 60 deletions
@@ -224,8 +224,10 @@ pub async fn get_alert_history(
Some(&user_email.user_id),
)
.await;
if max_query_range > 0 && (end_time - start_time) > max_query_range * 3600 * 1_000_000 {
start_time = end_time - max_query_range * 3600 * 1_000_000;
if let Some(clamped) =
config::utils::query_range::clamp_start_time(start_time, end_time, max_query_range)
{
start_time = clamped;
log::warn!(
"Start time for alert History api for org {org_id} updated as per max query range set for triggers stream"
)
+27 -10
View File
@@ -396,6 +396,7 @@ pub async fn get_roles(Path(_org_id): Path<String>) -> Response {
request_body(content = inline(RoleRequest), description = "RoleRequest", content_type = "application/json"),
responses(
(status = 200, description = "Success", content_type = "application/json", body = Object),
(status = 400, description = "Invalid role name, permission or user id; nothing is applied if any entry is invalid", content_type = "application/json", body = ()),
(status = 500, description = "Failure", content_type = "application/json", body = ()),
),
extensions(
@@ -407,6 +408,16 @@ pub async fn update_role(
Path((org_id, role_id)): Path<(String, String)>,
Json(update_role): Json<RoleRequest>,
) -> Response {
if let Err(msg) = o2_openfga::authorizer::roles::validate_role_update(
&org_id,
&role_id,
&update_role.add,
&update_role.remove,
update_role.add_users.as_ref(),
update_role.remove_users.as_ref(),
) {
return MetaHttpResponse::bad_request(msg);
}
match o2_openfga::authorizer::roles::update_role(
&org_id,
&role_id,
@@ -1478,28 +1489,34 @@ mod tests {
#[cfg(feature = "enterprise")]
{
init_ofga_test();
let role_request = o2_dex::meta::auth::RoleRequest {
add: vec![o2_dex::meta::auth::O2EntityAuthorization {
object: "permission1".to_string(),
permission: o2_dex::meta::auth::Permission::AllowAll,
}],
remove: vec![o2_dex::meta::auth::O2EntityAuthorization {
object: "permission2".to_string(),
permission: o2_dex::meta::auth::Permission::AllowAll,
}],
let grant = |object: &str| o2_dex::meta::auth::O2EntityAuthorization {
object: object.to_string(),
permission: o2_dex::meta::auth::Permission::AllowAll,
};
let role_request = |object: &str| o2_dex::meta::auth::RoleRequest {
add: vec![grant(object)],
remove: vec![grant("stream:permission2")],
add_users: Some(HashSet::from_iter(vec!["user1".to_string()])),
remove_users: Some(HashSet::from_iter(vec!["user2".to_string()])),
};
let resp = update_role(
Path(("test_org".to_string(), "test_role".to_string())),
Json(role_request),
Json(role_request("stream:permission1")),
)
.await;
assert_eq!(
extract_status_code(&resp),
StatusCode::INTERNAL_SERVER_ERROR
);
// A type the model lacks is the client's error, not OpenFGA's 400 surfaced as a 500.
let resp = update_role(
Path(("test_org".to_string(), "test_role".to_string())),
Json(role_request("no_such_type:permission1")),
)
.await;
assert_eq!(extract_status_code(&resp), StatusCode::BAD_REQUEST);
}
#[cfg(not(feature = "enterprise"))]
@@ -568,11 +568,7 @@ async fn clamp_to_max_query_range(org_id: &str, user_id: &str, start: i64, end:
};
let max_hours =
get_settings_max_query_range(settings.max_query_range, org_id, Some(user_id)).await;
if max_hours > 0 && (end - start) > max_hours * 3600 * 1_000_000 {
end - max_hours * 3600 * 1_000_000
} else {
start
}
config::utils::query_range::clamp_start_time(start, end, max_hours).unwrap_or(start)
}
#[cfg(feature = "enterprise")]
@@ -1096,8 +1096,10 @@ pub async fn get_workflow_history(
Some(&user_email.user_id),
)
.await;
if max_query_range > 0 && (end_time - start_time) > max_query_range * 3600 * 1_000_000 {
start_time = end_time - max_query_range * 3600 * 1_000_000;
if let Some(clamped) =
config::utils::query_range::clamp_start_time(start_time, end_time, max_query_range)
{
start_time = clamped;
log::warn!(
"Start time for alert History api for org {org_id} updated as per max query range set for triggers stream"
)
@@ -154,8 +154,10 @@ pub async fn get_pipeline_history(
Some(&user_email.user_id),
)
.await;
if max_query_range > 0 && (end_time - start_time) > max_query_range * 3600 * 1_000_000 {
start_time = end_time - max_query_range * 3600 * 1_000_000;
if let Some(clamped) =
config::utils::query_range::clamp_start_time(start_time, end_time, max_query_range)
{
start_time = clamped;
log::warn!(
"Start time for pipeline History api for org {org_id} updated as per max query range set for triggers stream"
)
+32
View File
@@ -541,6 +541,16 @@ async fn query_range(
}
},
};
if let Err(e) = validate_range_window(start, end) {
return (
StatusCode::BAD_REQUEST,
axum::Json(config::meta::promql::ApiFuncResponse::<()>::err_bad_data(
e,
Some(trace_id),
)),
)
.into_response();
}
if step == 0 {
step = promql::round_step((end - start) / promql::MAX_DATA_POINTS);
}
@@ -1201,6 +1211,19 @@ fn validate_metadata_params(
Ok((selector, start, end))
}
fn validate_range_window(start: i64, end: i64) -> Result<(), String> {
// `end - start` and the gRPC epoch conversion both overflow on a negative or reversed window.
if start < 0 {
return Err(format!(
"start must not be before the Unix epoch, got {start}"
));
}
if end < start {
return Err(format!("end {end} must not be before start {start}"));
}
Ok(())
}
/// prometheus formatting query expressions
// refer: https://prometheus.io/docs/prometheus/latest/querying/api/#formatting-query-expressions
@@ -1750,6 +1773,15 @@ impl promql_parser::util::ExprVisitor for MaxLookbackWindowVisitor {
mod tests {
use super::*;
#[test]
fn validate_range_window_rejects_negative_and_inverted_windows() {
assert!(validate_range_window(0, 0).is_ok());
assert!(validate_range_window(1_000, 2_000).is_ok());
assert!(validate_range_window(-1, 10).is_err());
assert!(validate_range_window(i64::MIN, 10).is_err());
assert!(validate_range_window(10, 9).is_err());
}
#[test]
fn metric_reads_check_permission_for_every_non_root_user() {
let text = include_str!("mod.rs");
+12 -6
View File
@@ -320,10 +320,13 @@ pub async fn search(
.await;
// Enrichment tables have no retention and must be searched in full; skip the clamp.
if stream_type != StreamType::EnrichmentTables
&& max_query_range > 0
&& (req.query.end_time - req.query.start_time) > max_query_range * 3600 * 1_000_000
&& let Some(clamped) = config::utils::query_range::clamp_start_time(
req.query.start_time,
req.query.end_time,
max_query_range,
)
{
req.query.start_time = req.query.end_time - max_query_range * 3600 * 1_000_000;
req.query.start_time = clamped;
range_error = format!(
"Query duration is modified due to query range restriction of {max_query_range} hours"
);
@@ -1807,10 +1810,13 @@ pub async fn result_schema(
.await;
// Enrichment tables have no retention and must be searched in full; skip the clamp.
if stream_type != StreamType::EnrichmentTables
&& max_query_range > 0
&& (req.query.end_time - req.query.start_time) > max_query_range * 3600 * 1_000_000
&& let Some(clamped) = config::utils::query_range::clamp_start_time(
req.query.start_time,
req.query.end_time,
max_query_range,
)
{
req.query.start_time = req.query.end_time - max_query_range * 3600 * 1_000_000;
req.query.start_time = clamped;
}
}
}
+6 -3
View File
@@ -256,10 +256,13 @@ pub async fn search_multi(
.await;
// Enrichment tables have no retention and must be searched in full; skip the clamp.
if stream_type != StreamType::EnrichmentTables
&& max_query_range > 0
&& (req.query.end_time - req.query.start_time) > max_query_range * 3600 * 1_000_000
&& let Some(clamped) = config::utils::query_range::clamp_start_time(
req.query.start_time,
req.query.end_time,
max_query_range,
)
{
req.query.start_time = req.query.end_time - max_query_range * 3600 * 1_000_000;
req.query.start_time = clamped;
range_error = format!(
"{} Query duration for stream {} is modified due to query range restriction of {} hours",
range_error, stream_name, max_query_range
@@ -181,10 +181,12 @@ pub async fn get_search_profile(
let max_query_range =
get_settings_max_query_range(settings.max_query_range, meta_org_id, Some(&user_id))
.await;
if max_query_range > 0
&& (req.query.end_time - req.query.start_time) > max_query_range * 3600 * 1_000_000
{
req.query.start_time = req.query.end_time - max_query_range * 3600 * 1_000_000;
if let Some(clamped) = config::utils::query_range::clamp_start_time(
req.query.start_time,
req.query.end_time,
max_query_range,
) {
req.query.start_time = clamped;
range_error = format!(
"Query duration is modified due to query range restriction of {max_query_range} hours"
);
+4 -2
View File
@@ -198,8 +198,10 @@ impl TraceListOptions {
}
pub fn restrict_range(&mut self, max_hours: i64) {
if max_hours > 0 && (self.range.1 - self.range.0) > max_hours * 3600 * 1_000_000 {
self.range.0 = self.range.1 - max_hours * 3600 * 1_000_000;
if let Some(clamped) =
config::utils::query_range::clamp_start_time(self.range.0, self.range.1, max_hours)
{
self.range.0 = clamped;
self.range_error = format!(
"Query duration is modified due to query range restriction of {max_hours} hours"
);
+4 -2
View File
@@ -188,8 +188,10 @@ pub async fn get_latest_sessions(
)
.await;
let mut range_error = String::new();
if max_query_range > 0 && (end_time - start_time) > max_query_range * 3600 * 1_000_000 {
start_time = end_time - max_query_range * 3600 * 1_000_000;
if let Some(clamped) =
config::utils::query_range::clamp_start_time(start_time, end_time, max_query_range)
{
start_time = clamped;
range_error = format!(
"Query duration is modified due to query range restriction of {max_query_range} hours"
);
+4 -2
View File
@@ -155,8 +155,10 @@ pub async fn get_latest_users(
)
.await;
let mut range_error = String::new();
if max_query_range > 0 && (end_time - start_time) > max_query_range * 3600 * 1_000_000 {
start_time = end_time - max_query_range * 3600 * 1_000_000;
if let Some(clamped) =
config::utils::query_range::clamp_start_time(start_time, end_time, max_query_range)
{
start_time = clamped;
range_error = format!(
"Query duration is modified due to query range restriction of {max_query_range} hours"
);