cascading permissions from dash to metrics

This commit is contained in:
dal 2025-07-16 12:22:04 -06:00
parent d00313131e
commit 7cee45916d
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
5 changed files with 201 additions and 121 deletions

View File

@ -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::<Uuid>(&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 {

View File

@ -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.");
// 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.");
// 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"));
}
}
let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id)
.await
.unwrap_or(false);
// 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).");
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;
}
}
}

View File

@ -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.");
// 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.");
// 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"));
}
}
let has_dashboard_access = sharing::check_metric_dashboard_access(metric_id, &user.id)
.await
.unwrap_or(false);
// 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).");
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;
}
}
}

View File

@ -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<bool>` - 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<bool, diesel::result::Error> {
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::<Uuid>(&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::<Uuid>(&mut conn)
.await
.optional()?;
Ok(has_public_access.is_some())
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -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};