diff --git a/src/common/infra/ofga/mod.rs b/src/common/infra/ofga/mod.rs index 3afa01f240..597ba437ea 100644 --- a/src/common/infra/ofga/mod.rs +++ b/src/common/infra/ofga/mod.rs @@ -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 = 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 { diff --git a/src/common/utils/auth.rs b/src/common/utils/auth.rs index 8fac7843f7..1db0a24422 100644 --- a/src/common/utils/auth.rs +++ b/src/common/utils/auth.rs @@ -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(), diff --git a/src/config/src/meta/search.rs b/src/config/src/meta/search.rs index 4a9035810a..ba02ab7248 100644 --- a/src/config/src/meta/search.rs +++ b/src/config/src/meta/search.rs @@ -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`" )), } } diff --git a/src/handler/http/models/alerts/mod.rs b/src/handler/http/models/alerts/mod.rs index fd79ed7500..b6f52a2934 100644 --- a/src/handler/http/models/alerts/mod.rs +++ b/src/handler/http/models/alerts/mod.rs @@ -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 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 for meta_search::SearchEventType { SearchEventType::DerivedStream => Self::DerivedStream, SearchEventType::SearchJob => Self::SearchJob, SearchEventType::Download => Self::Download, + SearchEventType::Insights => Self::Insights, } } } diff --git a/src/handler/http/request/alerts/mod.rs b/src/handler/http/request/alerts/mod.rs index 41a1ccc57a..df65978289 100644 --- a/src/handler/http/request/alerts/mod.rs +++ b/src/handler/http/request/alerts/mod.rs @@ -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 { diff --git a/src/handler/http/request/authz/fga.rs b/src/handler/http/request/authz/fga.rs index 5f7d2332c9..fefe3963fb 100644 --- a/src/handler/http/request/authz/fga.rs +++ b/src/handler/http/request/authz/fga.rs @@ -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) -> 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::>(); @@ -990,6 +990,12 @@ pub async fn get_resources(Path(_org_id): Path) -> Response { .into_iter() .filter(|r| !NON_CLOUD_RESOURCE_KEYS.contains(&r.key)) .collect::>(); + + let remove_keys = remove_resource_keys(); + let resources = resources + .into_iter() + .filter(|r| !remove_keys.contains(&r.key)) + .collect::>(); Json(resources).into_response() } diff --git a/src/handler/http/request/patterns/mod.rs b/src/handler/http/request/patterns/mod.rs index a502745fea..f9b30f4bc2 100644 --- a/src/handler/http/request/patterns/mod.rs +++ b/src/handler/http/request/patterns/mod.rs @@ -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 { diff --git a/src/handler/http/request/search/mod.rs b/src/handler/http/request/search/mod.rs index 6cdbf48c47..82ebb933a1 100644 --- a/src/handler/http/request/search/mod.rs +++ b/src/handler/http/request/search/mod.rs @@ -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; } diff --git a/src/handler/http/request/search/search_inspector.rs b/src/handler/http/request/search/search_inspector.rs index 391598a83d..07b5ba872b 100644 --- a/src/handler/http/request/search/search_inspector.rs +++ b/src/handler/http/request/search/search_inspector.rs @@ -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; } diff --git a/src/handler/http/request/search/search_job.rs b/src/handler/http/request/search/search_job.rs index 7c649babdc..4c71423755 100644 --- a/src/handler/http/request/search/search_job.rs +++ b/src/handler/http/request/search/search_job.rs @@ -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 = 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); } diff --git a/src/handler/http/request/search/search_stream.rs b/src/handler/http/request/search/search_stream.rs index de20d50bc4..0c0155329f 100644 --- a/src/handler/http/request/search/search_stream.rs +++ b/src/handler/http/request/search/search_stream.rs @@ -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")] diff --git a/src/handler/http/request/search/utils.rs b/src/handler/http/request/search/utils.rs index d6c874b6f4..f9e0c52641 100644 --- a/src/handler/http/request/search/utils.rs +++ b/src/handler/http/request/search/utils.rs @@ -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 { 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(), diff --git a/src/infra/src/table/alerts/intermediate.rs b/src/infra/src/table/alerts/intermediate.rs index c6aad32bc0..f1ea5db9af 100644 --- a/src/infra/src/table/alerts/intermediate.rs +++ b/src/infra/src/table/alerts/intermediate.rs @@ -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 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 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 for QuerySearchEventType { MetaSearchEventType::DerivedStream => Self::DerivedStream, MetaSearchEventType::SearchJob => Self::SearchJob, MetaSearchEventType::Download => Self::Download, + MetaSearchEventType::Insights => Self::Insights, } } } @@ -530,6 +535,7 @@ impl From for MetaSearchEventType { QuerySearchEventType::DerivedStream => Self::DerivedStream, QuerySearchEventType::SearchJob => Self::SearchJob, QuerySearchEventType::Download => Self::Download, + QuerySearchEventType::Insights => Self::Insights, } } } diff --git a/web/src/components/iam/roles/EditRole.vue b/web/src/components/iam/roles/EditRole.vue index fe1419cdd7..2a339fd984 100644 --- a/web/src/components/iam/roles/EditRole.vue +++ b/web/src/components/iam/roles/EditRole.vue @@ -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; + } }); }; diff --git a/web/src/plugins/traces/metrics/TracesAnalysisDashboard.vue b/web/src/plugins/traces/metrics/TracesAnalysisDashboard.vue index 755ab49a4e..ecb0cdd311 100644 --- a/web/src/plugins/traces/metrics/TracesAnalysisDashboard.vue +++ b/web/src/plugins/traces/metrics/TracesAnalysisDashboard.vue @@ -338,7 +338,7 @@ along with this program. If not, see . :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"