From d00313131e026005e21be1068c9703796d32c51e Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 16 Jul 2025 11:42:47 -0600 Subject: [PATCH 1/3] Add additional logging for session error --- apps/web/src/lib/supabase/getSupabaseUserContext.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/src/lib/supabase/getSupabaseUserContext.ts b/apps/web/src/lib/supabase/getSupabaseUserContext.ts index ac36749fe..390759435 100644 --- a/apps/web/src/lib/supabase/getSupabaseUserContext.ts +++ b/apps/web/src/lib/supabase/getSupabaseUserContext.ts @@ -11,7 +11,11 @@ export const getSupabaseUserContext = async (preemptiveRefreshMinutes = 5) => { const supabase = await createClient(); // Get the session first - let { data: sessionData } = await supabase.auth.getSession(); + let { data: sessionData, error: sessionError } = await supabase.auth.getSession(); + + if (sessionError) { + console.error('Error getting session:', sessionError); + } // Check if we need to refresh the session if (sessionData.session) { From 14e63fb278e90c6c088b99f05ab51af9fe0d36c1 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 16 Jul 2025 12:08:35 -0600 Subject: [PATCH 2/3] flush sync for access token + additional logging --- apps/web/src/api/createAxiosInstance.ts | 4 ++++ apps/web/src/app/app/layout.tsx | 2 +- .../context/Supabase/SupabaseContextProvider.tsx | 15 ++++++++++++++- .../src/lib/supabase/getSupabaseUserContext.ts | 12 +++++++++++- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/web/src/api/createAxiosInstance.ts b/apps/web/src/api/createAxiosInstance.ts index 191aa1e01..fa2adaa64 100644 --- a/apps/web/src/api/createAxiosInstance.ts +++ b/apps/web/src/api/createAxiosInstance.ts @@ -51,6 +51,10 @@ export const defaultAxiosRequestHandler = async ( token = (await options?.checkTokenValidity()?.then((res) => res?.access_token || '')) || ''; } + if (!token) { + throw new Error('User authentication error - no token found'); + } + (config.headers as AxiosRequestHeaders).Authorization = `Bearer ${token}`; return config; diff --git a/apps/web/src/app/app/layout.tsx b/apps/web/src/app/app/layout.tsx index b64fc7329..65b8f27ca 100644 --- a/apps/web/src/app/app/layout.tsx +++ b/apps/web/src/app/app/layout.tsx @@ -28,7 +28,7 @@ export default async function Layout({ const userInfoState = queryClient.getQueryState(queryKeys.userGetUserMyself.queryKey); - const is402Error = userInfoState?.status === 'error' && userInfoState?.error?.status === 402; + const is402Error = userInfoState?.status === 'error' && userInfoState?.error?.status === 402; //402 is the payment required error code if (is402Error) { return ; diff --git a/apps/web/src/context/Supabase/SupabaseContextProvider.tsx b/apps/web/src/context/Supabase/SupabaseContextProvider.tsx index 0c1c7f380..f4f20323d 100644 --- a/apps/web/src/context/Supabase/SupabaseContextProvider.tsx +++ b/apps/web/src/context/Supabase/SupabaseContextProvider.tsx @@ -7,6 +7,9 @@ import { checkTokenValidityFromServer } from '@/api/buster_rest/nextjs/auth'; import { useMemoizedFn } from '@/hooks/useMemoizedFn'; import { millisecondsFromUnixTimestamp } from '@/lib/timestamp'; import type { UseSupabaseUserContextType } from '@/lib/supabase'; +import { timeout } from '@/lib/timeout'; +import { useBusterNotifications } from '../BusterNotifications'; +import { flushSync } from 'react-dom'; const PREEMTIVE_REFRESH_MINUTES = 5; @@ -16,6 +19,7 @@ const useSupabaseContextInternal = ({ supabaseContext: UseSupabaseUserContextType; }) => { const refreshTimerRef = useRef | undefined>(undefined); + const { openErrorNotification, openInfoMessage } = useBusterNotifications(); const [accessToken, setAccessToken] = useState(supabaseContext.accessToken || ''); const isAnonymousUser = !supabaseContext.user?.id || supabaseContext.user?.is_anonymous === true; @@ -45,7 +49,8 @@ const useSupabaseContextInternal = ({ accessToken, preemptiveRefreshMinutes: PREEMTIVE_REFRESH_MINUTES }); - onUpdateToken({ accessToken: res.access_token, expiresAt: res.expires_at }); + await onUpdateToken({ accessToken: res.access_token, expiresAt: res.expires_at }); + await timeout(25); return res; } @@ -55,6 +60,11 @@ const useSupabaseContextInternal = ({ }; } catch (e) { console.error(e); + openErrorNotification({ + title: 'Error checking user authentication', + description: 'Please try again later', + duration: 120 * 1000 //2 minutes + }); throw e; } }); @@ -62,6 +72,9 @@ const useSupabaseContextInternal = ({ const onUpdateToken = useMemoizedFn( async ({ accessToken, expiresAt: _expiresAt }: { accessToken: string; expiresAt: number }) => { setAccessToken(accessToken); + flushSync(() => { + openInfoMessage('Token refreshed'); + }); } ); diff --git a/apps/web/src/lib/supabase/getSupabaseUserContext.ts b/apps/web/src/lib/supabase/getSupabaseUserContext.ts index 390759435..2bff75a98 100644 --- a/apps/web/src/lib/supabase/getSupabaseUserContext.ts +++ b/apps/web/src/lib/supabase/getSupabaseUserContext.ts @@ -33,10 +33,15 @@ export const getSupabaseUserContext = async (preemptiveRefreshMinutes = 5) => { } // Get user data - const { data: userData } = await supabase.auth.getUser(); + const { data: userData, error: userError } = await supabase.auth.getUser(); + + if (userError) { + console.error('Error getting user:', userData, userError); + } if (!userData.user) { const { session: anonSession } = await signInWithAnonymousUser(); + console.info('created anon session', anonSession); return { user: anonSession?.user || null, accessToken: anonSession?.access_token @@ -46,6 +51,11 @@ export const getSupabaseUserContext = async (preemptiveRefreshMinutes = 5) => { const user = userData.user; const accessToken = sessionData.session?.access_token; const refreshToken = sessionData.session?.refresh_token; + + if (!accessToken) { + console.error('No access token found for user:', user); + } + return { user, accessToken, refreshToken }; }; From 7cee45916df4dd4fe5af2e702eebfa14c8e60786 Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 16 Jul 2025 12:22:04 -0600 Subject: [PATCH 3/3] cascading permissions from dash to metrics --- .../src/metrics/get_metric_data_handler.rs | 50 +++------- .../get_metric_for_dashboard_handler.rs | 95 +++++++++++-------- .../src/metrics/get_metric_handler.rs | 95 +++++++++++-------- .../libs/sharing/src/asset_access_checks.rs | 80 +++++++++++++++- apps/api/libs/sharing/src/lib.rs | 2 +- 5 files changed, 201 insertions(+), 121 deletions(-) diff --git a/apps/api/libs/handlers/src/metrics/get_metric_data_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_data_handler.rs index 163fc6fde..47f5268e4 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_data_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_data_handler.rs @@ -1,11 +1,10 @@ use anyhow::{anyhow, Result}; -use chrono::Utc; use database::{ pool::get_pg_pool, - schema::{dashboard_files, metric_files, metric_files_to_dashboard_files}, + schema::metric_files, types::{data_metadata::DataMetadata, MetricYml}, }; -use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use indexmap::IndexMap; use middleware::AuthenticatedUser; @@ -68,41 +67,18 @@ pub async fn get_metric_data_handler( if is_permission_error { tracing::warn!( - "Initial metric access failed due to potential permission issue: {}. Checking public dashboard access.", + "Initial metric access failed due to potential permission issue: {}. Checking dashboard access.", e ); - // --- Step 3: Check if metric belongs to a valid public dashboard --- - let mut conn_check = get_pg_pool().get().await?; - let now = Utc::now(); - - let public_dashboard_exists = match metric_files_to_dashboard_files::table - .inner_join(dashboard_files::table.on( - dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id), - )) - .filter(metric_files_to_dashboard_files::metric_file_id.eq(request.metric_id)) - .filter(dashboard_files::publicly_accessible.eq(true)) - .filter(dashboard_files::deleted_at.is_null()) - .filter( - dashboard_files::public_expiry_date - .is_null() - .or(dashboard_files::public_expiry_date.gt(now)), - ) - .select(dashboard_files::id) // Select any column to check existence - .first::(&mut conn_check) // Try to get the first matching ID + // Check if user has access to ANY dashboard containing this metric (including public dashboards) + let has_dashboard_access = sharing::check_metric_dashboard_access(&request.metric_id, &user.id) .await - { - Ok(id) => Some(id), - Err(diesel::NotFound) => None, - Err(e) => { - tracing::error!("Error checking if public dashboard exists: {}", e); - return Err(anyhow!("Error checking if public dashboard exists: {}", e)); - } - }; + .unwrap_or(false); - if public_dashboard_exists.is_some() { - // --- Step 4: Public dashboard found, fetch metric bypassing permissions --- - tracing::info!("Found associated public dashboard. Fetching metric definition without direct permissions."); + if has_dashboard_access { + // User has access to a dashboard containing this metric + tracing::info!("Found associated dashboard with user access. Fetching metric with dashboard context."); match get_metric_for_dashboard_handler( &request.metric_id, &user, @@ -113,19 +89,19 @@ pub async fn get_metric_data_handler( { Ok(metric_via_dashboard) => { tracing::debug!( - "Successfully retrieved metric via public dashboard association." + "Successfully retrieved metric via dashboard association." ); metric_via_dashboard // Use this metric definition } Err(fetch_err) => { // If fetching via dashboard fails unexpectedly, return that error - tracing::error!("Failed to fetch metric via dashboard context even though public dashboard exists: {}", fetch_err); + tracing::error!("Failed to fetch metric via dashboard context: {}", fetch_err); return Err(fetch_err); } } } else { - // No public dashboard association found, return the original permission error - tracing::warn!("No valid public dashboard association found for metric. Returning original error."); + // No dashboard access, return the original permission error + tracing::warn!("No dashboard association found for metric. Returning original error."); return Err(e); } } else { diff --git a/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs index 88d81460c..27df1131b 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_for_dashboard_handler.rs @@ -157,48 +157,61 @@ pub async fn get_metric_for_dashboard_handler( tracing::debug!(metric_id = %metric_id, user_id = %user.id, ?permission, "Granting access via direct permission."); } } else { - // No sufficient direct/admin permission, check public access rules - tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking public access rules."); - if !metric_file.publicly_accessible { - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, insufficient direct permission)."); - return Err(anyhow!("You don't have permission to view this metric")); - } - tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible."); - - // Check if the public access has expired - if let Some(expiry_date) = metric_file.public_expiry_date { - tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date"); - if expiry_date < chrono::Utc::now() { - tracing::warn!(metric_id = %metric_id, "Public access expired"); - return Err(anyhow!("Public access to this metric has expired")); - } - } - - // Check if a password is required - tracing::debug!(metric_id = %metric_id, has_password = metric_file.public_password.is_some(), "Checking password requirement"); - if let Some(required_password) = &metric_file.public_password { - tracing::debug!(metric_id = %metric_id, "Password required. Checking provided password."); - match password { - Some(provided_password) => { - if provided_password != *required_password { - // Incorrect password provided - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Incorrect public password provided"); - return Err(anyhow!("Incorrect password for public access")); - } - // Correct password provided, grant CanView via public access - tracing::debug!(metric_id = %metric_id, user_id = %user.id, "Correct public password provided. Granting CanView."); - permission = AssetPermissionRole::CanView; - } - None => { - // Password required but none provided - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Public password required but none provided"); - return Err(anyhow!("public_password required for this metric")); - } - } - } else { - // Publicly accessible, not expired, and no password required - tracing::debug!(metric_id = %metric_id, "Public access granted (no password required)."); + // No sufficient direct/admin permission, check if user has access via a dashboard + tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking dashboard access."); + + let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id) + .await + .unwrap_or(false); + + if has_dashboard_access { + // User has access to a dashboard containing this metric, grant CanView + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via dashboard. Granting CanView."); permission = AssetPermissionRole::CanView; + } else { + // No dashboard access, check public access rules + tracing::debug!(metric_id = %metric_id, "No dashboard access. Checking public access rules."); + if !metric_file.publicly_accessible { + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, no dashboard access, insufficient direct permission)."); + return Err(anyhow!("You don't have permission to view this metric")); + } + tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible."); + + // Check if the public access has expired + if let Some(expiry_date) = metric_file.public_expiry_date { + tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date"); + if expiry_date < chrono::Utc::now() { + tracing::warn!(metric_id = %metric_id, "Public access expired"); + return Err(anyhow!("Public access to this metric has expired")); + } + } + + // Check if a password is required + tracing::debug!(metric_id = %metric_id, has_password = metric_file.public_password.is_some(), "Checking password requirement"); + if let Some(required_password) = &metric_file.public_password { + tracing::debug!(metric_id = %metric_id, "Password required. Checking provided password."); + match password { + Some(provided_password) => { + if provided_password != *required_password { + // Incorrect password provided + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Incorrect public password provided"); + return Err(anyhow!("Incorrect password for public access")); + } + // Correct password provided, grant CanView via public access + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "Correct public password provided. Granting CanView."); + permission = AssetPermissionRole::CanView; + } + None => { + // Password required but none provided + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Public password required but none provided"); + return Err(anyhow!("public_password required for this metric")); + } + } + } else { + // Publicly accessible, not expired, and no password required + tracing::debug!(metric_id = %metric_id, "Public access granted (no password required)."); + permission = AssetPermissionRole::CanView; + } } } diff --git a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs index 0b3957475..855a4dd3a 100644 --- a/apps/api/libs/handlers/src/metrics/get_metric_handler.rs +++ b/apps/api/libs/handlers/src/metrics/get_metric_handler.rs @@ -155,48 +155,61 @@ pub async fn get_metric_handler( tracing::debug!(metric_id = %metric_id, user_id = %user.id, ?permission, "Granting access via direct permission."); } } else { - // No sufficient direct/admin permission, check public access rules - tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking public access rules."); - if !metric_file.publicly_accessible { - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, insufficient direct permission)."); - return Err(anyhow!("You don't have permission to view this metric")); - } - tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible."); - - // Check if the public access has expired - if let Some(expiry_date) = metric_file.public_expiry_date { - tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date"); - if expiry_date < chrono::Utc::now() { - tracing::warn!(metric_id = %metric_id, "Public access expired"); - return Err(anyhow!("Public access to this metric has expired")); - } - } - - // Check if a password is required - tracing::debug!(metric_id = %metric_id, has_password = metric_file.public_password.is_some(), "Checking password requirement"); - if let Some(required_password) = &metric_file.public_password { - tracing::debug!(metric_id = %metric_id, "Password required. Checking provided password."); - match password { - Some(provided_password) => { - if provided_password != *required_password { - // Incorrect password provided - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Incorrect public password provided"); - return Err(anyhow!("Incorrect password for public access")); - } - // Correct password provided, grant CanView via public access - tracing::debug!(metric_id = %metric_id, user_id = %user.id, "Correct public password provided. Granting CanView."); - permission = AssetPermissionRole::CanView; - } - None => { - // Password required but none provided - tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Public password required but none provided"); - return Err(anyhow!("public_password required for this metric")); - } - } - } else { - // Publicly accessible, not expired, and no password required - tracing::debug!(metric_id = %metric_id, "Public access granted (no password required)."); + // No sufficient direct/admin permission, check if user has access via a dashboard + tracing::debug!(metric_id = %metric_id, "Insufficient direct/admin permission. Checking dashboard access."); + + let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id) + .await + .unwrap_or(false); + + if has_dashboard_access { + // User has access to a dashboard containing this metric, grant CanView + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "User has access via dashboard. Granting CanView."); permission = AssetPermissionRole::CanView; + } else { + // No dashboard access, check public access rules + tracing::debug!(metric_id = %metric_id, "No dashboard access. Checking public access rules."); + if !metric_file.publicly_accessible { + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Permission denied (not public, no dashboard access, insufficient direct permission)."); + return Err(anyhow!("You don't have permission to view this metric")); + } + tracing::debug!(metric_id = %metric_id, "Metric is publicly accessible."); + + // Check if the public access has expired + if let Some(expiry_date) = metric_file.public_expiry_date { + tracing::debug!(metric_id = %metric_id, ?expiry_date, "Checking expiry date"); + if expiry_date < chrono::Utc::now() { + tracing::warn!(metric_id = %metric_id, "Public access expired"); + return Err(anyhow!("Public access to this metric has expired")); + } + } + + // Check if a password is required + tracing::debug!(metric_id = %metric_id, has_password = metric_file.public_password.is_some(), "Checking password requirement"); + if let Some(required_password) = &metric_file.public_password { + tracing::debug!(metric_id = %metric_id, "Password required. Checking provided password."); + match password { + Some(provided_password) => { + if provided_password != *required_password { + // Incorrect password provided + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Incorrect public password provided"); + return Err(anyhow!("Incorrect password for public access")); + } + // Correct password provided, grant CanView via public access + tracing::debug!(metric_id = %metric_id, user_id = %user.id, "Correct public password provided. Granting CanView."); + permission = AssetPermissionRole::CanView; + } + None => { + // Password required but none provided + tracing::warn!(metric_id = %metric_id, user_id = %user.id, "Public password required but none provided"); + return Err(anyhow!("public_password required for this metric")); + } + } + } else { + // Publicly accessible, not expired, and no password required + tracing::debug!(metric_id = %metric_id, "Public access granted (no password required)."); + permission = AssetPermissionRole::CanView; + } } } diff --git a/apps/api/libs/sharing/src/asset_access_checks.rs b/apps/api/libs/sharing/src/asset_access_checks.rs index 5ecdc56cd..a5a662a4c 100644 --- a/apps/api/libs/sharing/src/asset_access_checks.rs +++ b/apps/api/libs/sharing/src/asset_access_checks.rs @@ -1,4 +1,8 @@ -use database::enums::{AssetPermissionRole, UserOrganizationRole}; +use database::enums::{AssetPermissionRole, AssetType, IdentityType, UserOrganizationRole}; +use database::pool::get_pg_pool; +use database::schema::{asset_permissions, dashboard_files, metric_files_to_dashboard_files}; +use diesel::{BoolExpressionMethods, ExpressionMethods, JoinOnDsl, QueryDsl, OptionalExtension}; +use diesel_async::RunQueryDsl; use middleware::OrganizationMembership; use uuid::Uuid; @@ -39,6 +43,80 @@ pub fn check_permission_access( false } +/// Checks if a user has access to a metric through any associated dashboard. +/// +/// This function is used to implement permission cascading from dashboards to metrics. +/// If a user has access to any dashboard containing the metric (either through direct permissions +/// or if the dashboard is public), they get at least CanView permission. +/// +/// # Arguments +/// * `metric_id` - UUID of the metric to check +/// * `user_id` - UUID of the user to check permissions for +/// +/// # Returns +/// * `Result` - True if the user has access to any dashboard containing the metric, false otherwise +pub async fn check_metric_dashboard_access( + metric_id: &Uuid, + user_id: &Uuid, +) -> Result { + let mut conn = get_pg_pool().get().await.map_err(|e| { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UnableToSendCommand, + Box::new(e.to_string()), + ) + })?; + + // First check if user has direct access to any dashboard containing this metric + let has_direct_access = metric_files_to_dashboard_files::table + .inner_join( + dashboard_files::table + .on(dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id)), + ) + .inner_join( + asset_permissions::table.on( + asset_permissions::asset_id.eq(dashboard_files::id) + .and(asset_permissions::asset_type.eq(AssetType::DashboardFile)) + .and(asset_permissions::identity_id.eq(user_id)) + .and(asset_permissions::identity_type.eq(IdentityType::User)) + .and(asset_permissions::deleted_at.is_null()) + ), + ) + .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id)) + .filter(dashboard_files::deleted_at.is_null()) + .filter(metric_files_to_dashboard_files::deleted_at.is_null()) + .select(dashboard_files::id) + .first::(&mut conn) + .await + .optional()?; + + if has_direct_access.is_some() { + return Ok(true); + } + + // Now check if metric belongs to any PUBLIC dashboard (not expired) + let now = chrono::Utc::now(); + let has_public_access = metric_files_to_dashboard_files::table + .inner_join( + dashboard_files::table + .on(dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id)), + ) + .filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id)) + .filter(dashboard_files::deleted_at.is_null()) + .filter(metric_files_to_dashboard_files::deleted_at.is_null()) + .filter(dashboard_files::publicly_accessible.eq(true)) + .filter( + dashboard_files::public_expiry_date + .is_null() + .or(dashboard_files::public_expiry_date.gt(now)), + ) + .select(dashboard_files::id) + .first::(&mut conn) + .await + .optional()?; + + Ok(has_public_access.is_some()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/apps/api/libs/sharing/src/lib.rs b/apps/api/libs/sharing/src/lib.rs index bb717a629..6658b9ba4 100644 --- a/apps/api/libs/sharing/src/lib.rs +++ b/apps/api/libs/sharing/src/lib.rs @@ -19,4 +19,4 @@ pub use types::{ SerializableAssetPermission, UserInfo, }; pub use user_lookup::find_user_by_email; -pub use asset_access_checks::check_permission_access; +pub use asset_access_checks::{check_permission_access, check_metric_dashboard_access};