feat: introduce rbac for logs patterns, logs insights and cache refresh (#10662)

This commit is contained in:
Subhra264
2026-03-01 16:20:10 +08:00
committed by GitHub
parent 74c2d914e3
commit 90a279134b
15 changed files with 244 additions and 36 deletions
+18 -1
View File
@@ -31,7 +31,10 @@ use o2_openfga::{
add_tuple_for_pipeline, get_add_user_to_org_tuples, get_org_creation_tuples,
get_ownership_all_org_tuple, get_ownership_tuple, update_tuples,
},
meta::mapping::{NON_OWNING_ORG, OFGA_MODELS, OFGAModel},
meta::mapping::{
LOGS_INSIGHTS_KEY, LOGS_PATTERN_KEY, NON_OWNING_ORG, OFGA_MODELS, OFGAModel,
RESULT_LOGS_CACHE_KEY,
},
};
use crate::{
@@ -62,6 +65,7 @@ pub async fn init() -> Result<(), anyhow::Error> {
let mut need_ai_chat_permissions_migration = false;
let mut need_re_pattern_permission_migration = false;
let mut need_license_permission_migration = false;
let mut need_logs_pattern_insights_migration = false;
let existing_meta: Option<o2_openfga::meta::mapping::OFGAModel> =
match db::ofga::get_ofga_model().await {
@@ -236,6 +240,7 @@ pub async fn init() -> Result<(), anyhow::Error> {
let v0_0_18 = version_compare::Version::from("0.0.18").unwrap();
let v0_0_20 = version_compare::Version::from("0.0.20").unwrap();
let v0_0_21 = version_compare::Version::from("0.0.21").unwrap();
let v0_0_25 = version_compare::Version::from("0.0.25").unwrap();
if meta_version > v0_0_5 && existing_model_version < v0_0_6 {
need_pipeline_migration = true;
@@ -269,6 +274,13 @@ pub async fn init() -> Result<(), anyhow::Error> {
log::info!("[OFGA:Local] license permissions migration needed");
need_license_permission_migration = true;
}
if existing_model_version < v0_0_25 {
log::info!(
"[OFGA:Local] logs patterns, insights, cache delete permissions migration needed"
);
need_logs_pattern_insights_migration = true;
}
}
let mut tuples = vec![];
@@ -368,6 +380,11 @@ pub async fn init() -> Result<(), anyhow::Error> {
if need_license_permission_migration {
get_ownership_all_org_tuple(org_name, "license", &mut tuples);
}
if need_logs_pattern_insights_migration {
get_ownership_all_org_tuple(org_name, LOGS_INSIGHTS_KEY, &mut tuples);
get_ownership_all_org_tuple(org_name, LOGS_PATTERN_KEY, &mut tuples);
get_ownership_all_org_tuple(org_name, RESULT_LOGS_CACHE_KEY, &mut tuples);
}
}
if need_alert_folders_migration {
match migrations::migrate_alert_folders().await {
+1
View File
@@ -1042,6 +1042,7 @@ where
// service_streams APIs are org-level, not stream-specific
|| path.contains("/service_streams/_analytics")
|| path.contains("/service_streams/_correlate")
|| (url_len == 5 && path.ends_with("/patterns/extract"))
{
return Ok(AuthExtractor {
auth: auth_str.to_owned(),
+4 -1
View File
@@ -953,6 +953,7 @@ pub enum SearchEventType {
DerivedStream,
SearchJob,
Download,
Insights,
}
impl<'de> Deserialize<'de> for SearchEventType {
@@ -994,6 +995,7 @@ impl std::fmt::Display for SearchEventType {
Self::DerivedStream => write!(f, "derived_stream"),
Self::SearchJob => write!(f, "search_job"),
Self::Download => write!(f, "download"),
Self::Insights => write!(f, "insights"),
}
}
}
@@ -1012,8 +1014,9 @@ impl TryFrom<&str> for SearchEventType {
"derived_stream" | "derivedstream" => Ok(Self::DerivedStream),
"search_job" | "searchjob" => Ok(Self::SearchJob),
"download" => Ok(Self::Download),
"insights" => Ok(Self::Insights),
_ => Err(format!(
"invalid SearchEventType `{s}`, expected one of `ui`, `dashboards`, `reports`, `alerts`, `values`, `other`, `rum`, `derived_stream`, `search_job`"
"invalid SearchEventType `{s}`, expected one of `ui`, `dashboards`, `reports`, `alerts`, `values`, `other`, `rum`, `derived_stream`, `search_job`, `insights`"
)),
}
}
+3
View File
@@ -384,6 +384,7 @@ pub enum SearchEventType {
DerivedStream,
SearchJob,
Download,
Insights,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, ToSchema, Hash)]
@@ -568,6 +569,7 @@ impl From<meta_search::SearchEventType> for SearchEventType {
meta_search::SearchEventType::DerivedStream => Self::DerivedStream,
meta_search::SearchEventType::SearchJob => Self::SearchJob,
meta_search::SearchEventType::Download => Self::Download,
meta_search::SearchEventType::Insights => Self::Insights,
}
}
}
@@ -748,6 +750,7 @@ impl From<SearchEventType> for meta_search::SearchEventType {
SearchEventType::DerivedStream => Self::DerivedStream,
SearchEventType::SearchJob => Self::SearchJob,
SearchEventType::Download => Self::Download,
SearchEventType::Insights => Self::Insights,
}
}
}
+4 -1
View File
@@ -31,7 +31,9 @@ use svix_ksuid::Ksuid;
#[cfg(feature = "enterprise")]
use {
crate::common::utils::auth::check_permissions,
crate::handler::http::request::search::utils::check_stream_permissions,
crate::handler::http::request::search::utils::{
StreamPermissionResourceType, check_stream_permissions,
},
};
use crate::{
@@ -748,6 +750,7 @@ pub async fn generate_sql(
&org_id,
&user_email.user_id,
&stream_type,
StreamPermissionResourceType::Search,
)
.await
{
+7 -1
View File
@@ -981,7 +981,7 @@ pub async fn get_group_details(Path(_path): Path<(String, String)>) -> Response
pub async fn get_resources(Path(_org_id): Path<String>) -> Response {
#[cfg(feature = "cloud")]
use o2_openfga::meta::mapping::NON_CLOUD_RESOURCE_KEYS;
use o2_openfga::meta::mapping::Resource;
use o2_openfga::meta::mapping::{Resource, remove_resource_keys};
let resources = o2_openfga::meta::mapping::OFGA_MODELS
.values()
.collect::<Vec<&Resource>>();
@@ -990,6 +990,12 @@ pub async fn get_resources(Path(_org_id): Path<String>) -> Response {
.into_iter()
.filter(|r| !NON_CLOUD_RESOURCE_KEYS.contains(&r.key))
.collect::<Vec<&Resource>>();
let remove_keys = remove_resource_keys();
let resources = resources
.into_iter()
.filter(|r| !remove_keys.contains(&r.key))
.collect::<Vec<&Resource>>();
Json(resources).into_response()
}
+4 -1
View File
@@ -21,7 +21,9 @@ use axum::{
use o2_enterprise::enterprise::common::config::get_config as get_o2_config;
#[cfg(feature = "enterprise")]
use crate::handler::http::request::search::utils::check_stream_permissions;
use crate::handler::http::request::search::utils::{
StreamPermissionResourceType, check_stream_permissions,
};
use crate::{
common::{meta::http::HttpResponse as MetaHttpResponse, utils::auth::UserEmail},
handler::http::extractors::Headers,
@@ -159,6 +161,7 @@ pub async fn extract_patterns(
&org_id,
&user_id,
&config::meta::stream::StreamType::Logs,
StreamPermissionResourceType::PatternExtract,
)
.await
{
+17 -5
View File
@@ -41,7 +41,7 @@ use hashbrown::HashMap;
use http::HeaderMap;
use tracing::{Instrument, Span};
#[cfg(feature = "enterprise")]
use utils::check_stream_permissions;
use utils::{StreamPermissionResourceType, check_stream_permissions};
#[cfg(feature = "cloud")]
use crate::service::organization::is_org_in_free_trial_period;
@@ -387,8 +387,14 @@ pub async fn search(
// Check permissions on stream
#[cfg(feature = "enterprise")]
if let Some(res) =
check_stream_permissions(&stream_name, &org_id, user_id, &stream_type).await
if let Some(res) = check_stream_permissions(
&stream_name,
&org_id,
user_id,
&stream_type,
StreamPermissionResourceType::Search,
)
.await
{
return res;
}
@@ -1884,8 +1890,14 @@ pub async fn result_schema(
// Check permissions on stream
#[cfg(feature = "enterprise")]
if let Some(res) =
check_stream_permissions(&stream_name, &org_id, user_id, &stream_type).await
if let Some(res) = check_stream_permissions(
&stream_name,
&org_id,
user_id,
&stream_type,
StreamPermissionResourceType::Search,
)
.await
{
return res;
}
@@ -34,7 +34,9 @@ use serde_json;
use tracing::{Instrument, Span};
#[cfg(feature = "enterprise")]
use crate::handler::http::request::search::utils::check_stream_permissions;
use crate::handler::http::request::search::utils::{
StreamPermissionResourceType, check_stream_permissions,
};
#[cfg(feature = "enterprise")]
use crate::service::search::sql::visitor::cipher_key::get_cipher_key_names;
use crate::{
@@ -189,7 +191,14 @@ pub async fn get_search_profile(
// Check permissions on stream
#[cfg(feature = "enterprise")]
if let Some(res) = check_stream_permissions(&stream_name, org_id, &user_id, &stream_type).await
if let Some(res) = check_stream_permissions(
&stream_name,
org_id,
&user_id,
&stream_type,
StreamPermissionResourceType::Search,
)
.await
{
return res;
}
+18 -5
View File
@@ -23,7 +23,8 @@ use config::meta::search::Request;
#[cfg(feature = "enterprise")]
use {
crate::handler::http::request::search::{
query_manager::cancel_query_inner, utils::check_stream_permissions,
query_manager::cancel_query_inner,
utils::{StreamPermissionResourceType, check_stream_permissions},
},
crate::service::search_jobs::{get_result, merge_response},
crate::{
@@ -181,8 +182,14 @@ pub async fn submit_job(
// Check permissions on stream
for stream_name in stream_names.iter() {
if let Some(res) =
check_stream_permissions(stream_name, &org_id, &user_id, &stream_type).await
if let Some(res) = check_stream_permissions(
stream_name,
&org_id,
&user_id,
&stream_type,
StreamPermissionResourceType::Search,
)
.await
{
return res;
}
@@ -714,8 +721,14 @@ async fn check_permissions(job: &JobModel, org_id: &str, user_id: &str) -> Optio
let stream_type = StreamType::from(job.stream_type.as_str());
let stream_names: Vec<String> = json::from_str(&job.stream_names).unwrap();
for stream_name in stream_names.iter() {
if let Some(res) =
check_stream_permissions(stream_name, org_id, user_id, &stream_type).await
if let Some(res) = check_stream_permissions(
stream_name,
org_id,
user_id,
&stream_type,
StreamPermissionResourceType::Search,
)
.await
{
return Some(res);
}
@@ -45,8 +45,11 @@ use tracing::Span;
use crate::common::utils::http::get_extract_patterns_from_request;
#[cfg(feature = "enterprise")]
use crate::{
common::meta::search::AuditContext, common::utils::auth::check_permissions,
handler::http::request::search::utils::check_stream_permissions,
common::meta::search::AuditContext,
common::utils::auth::check_permissions,
handler::http::request::search::utils::{
StreamPermissionResourceType, check_stream_permissions,
},
service::self_reporting::audit,
};
use crate::{
@@ -364,16 +367,28 @@ pub async fn search_http2_stream(
}
}
req.clear_cache = get_clear_cache_from_request(&query);
// Check if user has edit permissions when clear_cache is requested
#[cfg(feature = "enterprise")]
if get_clear_cache_from_request(&query) {
if req.clear_cache {
use o2_openfga::meta::mapping::{RESULT_LOGS_CACHE_KEY, is_cache_rbac_enabled};
// We only check DELETE cache rbac for logs stream type
let (permission_type, method) =
if stream_type.eq(&StreamType::Logs) && is_cache_rbac_enabled() {
// check if cache rbac is enabled
(RESULT_LOGS_CACHE_KEY, "DELETE")
} else {
(stream_type.as_str(), "PUT")
};
for stream_name in stream_names.iter() {
if !check_permissions(
stream_name,
&org_id,
&user_id,
stream_type.as_str(),
"PUT",
permission_type,
method,
None,
)
.await
@@ -403,7 +418,6 @@ pub async fn search_http2_stream(
}
// Set use_cache from query params
req.clear_cache = get_clear_cache_from_request(&query);
req.use_cache = get_use_cache_from_request(&query) && !req.clear_cache;
// Set search type if not set
@@ -451,11 +465,25 @@ pub async fn search_http2_stream(
.and_then(|event_type| get_search_event_context_from_request(event_type, &query));
}
#[cfg(feature = "enterprise")]
// Check permissions for each stream
let permission_resource_type =
if req.search_type == Some(config::meta::search::SearchEventType::Insights) {
StreamPermissionResourceType::Insights
} else {
StreamPermissionResourceType::Search
};
#[cfg(feature = "enterprise")]
for stream_name in stream_names.iter() {
if let Some(res) =
check_stream_permissions(stream_name, &org_id, &user_id, &stream_type).await
if let Some(res) = check_stream_permissions(
stream_name,
&org_id,
&user_id,
&stream_type,
permission_resource_type,
)
.await
{
// Add audit before closing
#[cfg(feature = "enterprise")]
@@ -843,8 +871,14 @@ pub async fn values_http2_stream(
// Check permissions for each stream
#[cfg(feature = "enterprise")]
for stream_name in stream_names.iter() {
if let Some(res) =
check_stream_permissions(stream_name, &org_id, &user_id, &stream_type).await
if let Some(res) = check_stream_permissions(
stream_name,
&org_id,
&user_id,
&stream_type,
StreamPermissionResourceType::Search,
)
.await
{
// Add audit before closing
#[cfg(feature = "enterprise")]
+39 -8
View File
@@ -33,11 +33,22 @@ use {
service::users::get_user,
},
config::meta::user::User,
o2_openfga::meta::mapping::OFGA_MODELS,
o2_openfga::{
config::get_config,
meta::mapping::{LOGS_INSIGHTS_KEY, LOGS_PATTERN_KEY, OFGA_MODELS},
},
};
use crate::service::search::sql::Sql;
#[cfg(feature = "enterprise")]
#[derive(Clone, Copy)]
pub enum StreamPermissionResourceType {
Search,
PatternExtract,
Insights,
}
// Check permissions on stream
#[cfg(feature = "enterprise")]
pub async fn check_stream_permissions(
@@ -45,23 +56,43 @@ pub async fn check_stream_permissions(
org_id: &str,
user_id: &str,
stream_type: &StreamType,
permission_resource_type: StreamPermissionResourceType,
) -> Option<Response> {
if !is_root_user(user_id) {
let user: User = get_user(Some(org_id), user_id).await.unwrap();
let stream_type_str = stream_type.as_str();
let config = get_config();
let mut o2_model_type = "";
// If rbac for logs pattern and logs insights is enabled, use those resources for permission
// check
match permission_resource_type {
StreamPermissionResourceType::PatternExtract => {
if config.logs_pattern_rbac_enabled {
o2_model_type = LOGS_PATTERN_KEY;
}
}
StreamPermissionResourceType::Insights => {
if config.logs_pattern_rbac_enabled {
o2_model_type = LOGS_INSIGHTS_KEY;
}
}
_ => {}
};
if o2_model_type.is_empty() {
o2_model_type = OFGA_MODELS
.get(stream_type_str)
.map_or(stream_type_str, |model| model.key);
}
if !crate::handler::http::auth::validator::check_permissions(
user_id,
AuthExtractor {
auth: "".to_string(),
method: "GET".to_string(),
o2_type: format!(
"{}:{}",
OFGA_MODELS
.get(stream_type_str)
.map_or(stream_type_str, |model| model.key),
stream_name
),
o2_type: format!("{o2_model_type}:{stream_name}"),
org_id: org_id.to_string(),
bypass_check: false,
parent_id: "".to_string(),
@@ -441,6 +441,7 @@ pub enum QuerySearchEventType {
DerivedStream,
SearchJob,
Download,
Insights,
}
impl QuerySearchEventType {
@@ -454,6 +455,7 @@ impl QuerySearchEventType {
const DERIVED_STREAM: i16 = 7;
const SEARCH_JOB: i16 = 8;
const DOWNLOAD: i16 = 9;
const INSIGHTS: i16 = 10;
}
// Implementation for translating from intermediate representation into DB
@@ -471,6 +473,7 @@ impl From<QuerySearchEventType> for i16 {
QuerySearchEventType::DerivedStream => QuerySearchEventType::DERIVED_STREAM,
QuerySearchEventType::SearchJob => QuerySearchEventType::SEARCH_JOB,
QuerySearchEventType::Download => QuerySearchEventType::DOWNLOAD,
QuerySearchEventType::Insights => QuerySearchEventType::INSIGHTS,
}
}
}
@@ -492,6 +495,7 @@ impl TryFrom<i16> for QuerySearchEventType {
Self::DERIVED_STREAM => Ok(QuerySearchEventType::DerivedStream),
Self::SEARCH_JOB => Ok(QuerySearchEventType::SearchJob),
Self::DOWNLOAD => Ok(QuerySearchEventType::Download),
Self::INSIGHTS => Ok(QuerySearchEventType::Insights),
_ => Err(FromI16Error {
value,
ty: "SearchEventType".to_owned(),
@@ -513,6 +517,7 @@ impl From<MetaSearchEventType> for QuerySearchEventType {
MetaSearchEventType::DerivedStream => Self::DerivedStream,
MetaSearchEventType::SearchJob => Self::SearchJob,
MetaSearchEventType::Download => Self::Download,
MetaSearchEventType::Insights => Self::Insights,
}
}
}
@@ -530,6 +535,7 @@ impl From<QuerySearchEventType> for MetaSearchEventType {
QuerySearchEventType::DerivedStream => Self::DerivedStream,
QuerySearchEventType::SearchJob => Self::SearchJob,
QuerySearchEventType::Download => Self::Download,
QuerySearchEventType::Insights => Self::Insights,
}
}
}
+67
View File
@@ -635,6 +635,21 @@ const modifyResourcePermissions = (resource: Resource) => {
resource.permission.AllowDelete.show = false;
resource.permission.AllowPost.show = false;
}
if (
resource.resourceName === "logs_pattern" ||
resource.resourceName === "logs_insights"
) {
resource.permission.AllowList.show = false;
resource.permission.AllowDelete.show = false;
resource.permission.AllowPost.show = false;
resource.permission.AllowPut.show = false;
}
if (resource.resourceName === "logs_cache") {
resource.permission.AllowList.show = false;
resource.permission.AllowGet.show = false;
resource.permission.AllowPost.show = false;
resource.permission.AllowPut.show = false;
}
};
const getResourcePermissions = () => {
@@ -1388,6 +1403,9 @@ const getResourceEntities = (resource: Resource | Entity) => {
cipher_keys: getCipherKeys,
afolder: getAlertFolders,
re_patterns: getRePatterns,
logs_pattern: getLogsPatternStreams,
logs_insights: getLogsInsightsStreams,
logs_cache: getLogsCacheStreams,
};
return new Promise(async (resolve, reject) => {
@@ -1639,6 +1657,36 @@ const getLogs = async (resource: Resource | Entity) => {
});
};
const getLogsPatternStreams = async (resource: Resource | Entity) => {
const logs: any = await getStreams("logs", false);
updateResourceEntities("logs_pattern", ["name"], logs.list);
return new Promise((resolve) => {
resolve(true);
});
};
const getLogsInsightsStreams = async (resource: Resource | Entity) => {
const logs: any = await getStreams("logs", false);
updateResourceEntities("logs_insights", ["name"], logs.list);
return new Promise((resolve) => {
resolve(true);
});
};
const getLogsCacheStreams = async (resource: Resource | Entity) => {
const logs: any = await getStreams("logs", false);
updateResourceEntities("logs_cache", ["name"], logs.list);
return new Promise((resolve) => {
resolve(true);
});
};
const getIndexStreams = async (resource: Resource | Entity) => {
const indices: any = await getStreams("index", false);
@@ -1963,6 +2011,25 @@ const updateResourceEntities = (
top_level: !!resource.childs.find((child) => child.name === childName)
?.top_level,
});
// Hide non-applicable permissions for logs_pattern and logs_insights entities
if (
resourceName === "logs_pattern" ||
resourceName === "logs_insights"
) {
const entity = resource.entities[resource.entities.length - 1];
entity.permission.AllowList.show = false;
entity.permission.AllowDelete.show = false;
entity.permission.AllowPost.show = false;
entity.permission.AllowPut.show = false;
}
// Hide non-applicable permissions for logs_cache entities (only All and Delete)
if (resourceName === "logs_cache") {
const entity = resource.entities[resource.entities.length - 1];
entity.permission.AllowList.show = false;
entity.permission.AllowGet.show = false;
entity.permission.AllowPost.show = false;
entity.permission.AllowPut.show = false;
}
});
};
@@ -338,7 +338,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:viewOnly="false"
:allowAlertCreation="false"
:simplifiedPanelView="true"
searchType="dashboards"
:searchType="props.streamType === 'logs' ? 'insights' : 'dashboards'"
@variablesManagerReady="onVariablesManagerReady"
@onDeletePanel="handlePanelDelete"
class="tw:p-[0.4rem] trace-analysis-dashboards"