13 KiB
Sub-PRD: Enhance Dashboard Metric Permissions
Author: AI Assistant (Pair Programming with User) Date: 2023-10-27 Status: Proposed Parent PRD: Project: Granular Asset Permission Checks
1. Overview
This PRD describes the modifications needed for the get_dashboard_handler
to implement granular permission checks for each metric included in a dashboard. It involves leveraging the central permission helper and adjusting how metric data is fetched and represented in the response, potentially by modifying get_metric_handler
or how its results are processed.
Note on Concurrency: This work depends on the completion of the Refactor Sharing Permission Helper. Once the helper is available, this task can potentially be performed concurrently with the enhancements for the collection and data execution handlers.
2. Problem Statement
Currently, get_dashboard_handler
fetches associated metrics using get_metric_handler
. If get_metric_handler
fails (potentially due to permissions), the metric is silently filtered out from the dashboard response's metrics
map. Users aren't informed why a metric configured in the dashboard isn't visible, and they can't distinguish between a metric failing to load due to an error vs. lack of permissions.
3. Goals
- Modify
get_dashboard_handler
's metric fetching logic to handle permission denials gracefully. - Ensure that if a user lacks
CanView
permission for a metric configured in a dashboard, the metric is still included in the response'smetrics
map, but represented minimally withhas_access: false
. - Add the
has_access: bool
field to theBusterMetric
type. - Leverage the
check_specific_asset_access
helper for permission verification. - Note: This handler modification primarily affects the metadata served about the metric. Blocking the actual execution of the metric's SQL query for data retrieval will be handled by adding permission checks to the relevant data execution handler (as per the project PRD).
4. Non-Goals
- Changing the permission check logic for the dashboard itself.
- Modifying how metrics are associated with dashboards (i.e., the dashboard config).
5. Technical Design
Option A: Modify get_metric_handler
(Recommended)
-
Type Modification:
- Add
has_access: bool
toBusterMetric
struct (likely inlibs/handlers/src/metrics/types.rs
). Also ensure relevant nested types likeChartConfig
(note: it's fromdatabase::types::ChartConfig
) are handled during minimal object creation.// libs/handlers/src/metrics/types.rs (adjust path as needed) use database::types::ChartConfig; // Import the correct ChartConfig use database::enums::Verification; use chrono::{DateTime, Utc, TimeZone}; #[derive(Serialize, Deserialize, Debug, Clone)] // Add necessary derives pub struct BusterMetric { // --- Fields always present, even if no access --- pub id: Uuid, #[serde(default)] // Use default if loading minimal object pub name: String, pub has_access: bool, // New field // --- Fields only present if has_access = true --- #[serde(skip_serializing_if = "Option::is_none")] pub description: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub config: Option<MetricConfig>, // Or appropriate type #[serde(skip_serializing_if = "Option::is_none")] pub data: Option<Value>, // Or appropriate type #[serde(skip_serializing_if = "Option::is_none")] pub created_at: Option<DateTime<Utc>>, // ... other optional fields ... #[serde(skip_serializing_if = "Option::is_none")] pub version_number: Option<i32>, // ... etc ... } // Add default implementation if needed for minimal construction // Note: Default cannot be easily derived due to non-optional fields. // We will construct the minimal object manually. /* impl Default for BusterMetric { fn default() -> Self { Self { id: Uuid::nil(), // Or handle differently name: String::new(), has_access: false, description: None, config: None, data: None, created_at: None, version_number: None, // ... initialize other optional fields to None ... } } } */
- Add
-
Modify
get_metric_handler.rs
:- Initial Fetch & Check: Start by fetching the basic
metric_files
record and checking permission usingcheck_specific_asset_access
(orfetch_metric_file_with_permission
if it can be adapted to return the basic file even on permission denial).// Inside get_metric_handler... let mut conn = get_pg_pool().get().await?; let metric_id = /* get metric_id */; let user = /* get user */; // Fetch basic info first (needed for permission check and minimal response) let basic_metric_info = metric_files::table .filter(metric_files::id.eq(metric_id)) .filter(metric_files::deleted_at.is_null()) .select((metric_files::id, metric_files::name, metric_files::organization_id, metric_files::created_by)) .first::<(Uuid, String, Uuid, Uuid)>(&mut conn) // Fetch created_by too .await .optional()?; // Use optional() to handle not found gracefully let (id, name, org_id, created_by_id) = match basic_metric_info { Some(info) => info, None => return Err(anyhow!("Metric not found")), // Return Err if metric doesn't exist }; // Check permission let required_roles = [AssetPermissionRole::CanView]; let has_permission = sharing::check_specific_asset_access( &mut conn, user, &id, AssetType::MetricFile, org_id, &required_roles ).await?; // Propagate DB errors from check if has_permission { // Proceed to fetch full metric details (content, version history, etc.) as before // ... fetch full_metric_file ... // Construct full BusterMetric with has_access: true Ok(BusterMetric { id, name, // Use fetched name has_access: true, // ... populate all other fields from full_metric_file ... }) } else { // Construct minimal BusterMetric with has_access: false // Need to provide defaults for non-optional fields. let default_time = Utc.timestamp_opt(0, 0).unwrap(); // Use epoch for timestamps Ok(BusterMetric { id, metric_type: "metric".to_string(), name, // Use fetched name version_number: 0, // Default version description: None, // Optional file_name: "".to_string(), // Default empty time_frame: "".to_string(), // Default empty datasets: vec![], // Default empty data_source_id: "".to_string(), // Default empty error: None, // Optional chart_config: None, // Optional data_metadata: None, // Optional status: Verification::Unverified, // Default status evaluation_score: None, // Optional evaluation_summary: "".to_string(), // Default empty file: "".to_string(), // Default empty YAML created_at: default_time, // Default timestamp updated_at: default_time, // Default timestamp sent_by_id: created_by_id, // Use fetched creator ID sent_by_name: "(Restricted Access)".to_string(), // Placeholder name sent_by_avatar_url: None, // Optional code: None, // Optional (SQL query) dashboards: vec![], // Default empty collections: vec![], // Default empty versions: vec![], // Default empty permission: AssetPermissionRole::CanView, // Technically viewable, but content restricted sql: "-- Restricted Access --".to_string(), // Placeholder SQL individual_permissions: None, // Optional public_expiry_date: None, // Optional public_enabled_by: None, // Optional publicly_accessible: false, // Default public_password: None, // Optional // **Crucially set has_access to false** has_access: false, }) }
- Return Value: The handler now always returns
Ok(BusterMetric)
if the metric exists, differentiating access via thehas_access
flag. It only returnsErr
if the metric record itself is not found or if a database error occurs during the permission check or data fetching.
- Initial Fetch & Check: Start by fetching the basic
-
Modify
get_dashboard_handler.rs
:- The logic using
join_all
and collecting results into themetrics: HashMap<Uuid, BusterMetric>
map can remain largely the same, asget_metric_handler
will now consistently returnOk
for existing metrics. Errors genuinely representing fetch failures (metric not found, DB error) will still beErr
and should be logged/handled.// In get_dashboard_handler, processing results: let metric_results = join_all(metric_futures).await; let metrics: HashMap<Uuid, BusterMetric> = metric_results .into_iter() .filter_map(|result| match result { Ok(metric) => Some((metric.id, metric)), // metric includes has_access flag Err(e) => { // Log actual errors (metric not found, DB connection issues, etc.) tracing::error!("Failed to fetch metric details for dashboard (non-permission error): {}", e); None // Exclude metric if there was a real error } }) .collect();
- The logic using
Option B: Handle in get_dashboard_handler
(Less Recommended)
- Keep
get_metric_handler
mostly as-is (returningErr
on permission denied). - In
get_dashboard_handler
, afterjoin_all
, iterate through results. If a result isErr
, attempt a separate minimal query to fetch just theid
andname
for that metric ID. If successful, create a minimalBusterMetric { has_access: false, ... }
and add it to the map. If the minimal query fails (e.g., metric truly doesn't exist), log and omit. - Drawback: Less efficient (potential extra queries), splits logic.
Decision: Proceed with Option A.
- File Changes:
- Modify:
libs/handlers/src/metrics/get_metric_handler.rs
(or similar) - Modify:
libs/handlers/src/metrics/types.rs
(or similar, forBusterMetric
) - Modify:
libs/handlers/src/dashboards/get_dashboard_handler.rs
(minor changes to error handling/filtering logic afterjoin_all
)
- Modify:
6. Implementation Plan
- Modify
BusterMetric
type definition. - Refactor
get_metric_handler
to perform permission check upfront and return minimal object on denial. - Adjust result processing loop in
get_dashboard_handler
. - Add/update integration tests.
7. Testing Strategy
- Unit Tests (
get_metric_handler
):- Mock DB interactions.
- Test case: User lacks
CanView
-> returnsOk(BusterMetric { id, name, has_access: false, ... })
. - Test case: User has
CanView
-> returnsOk(BusterMetric { has_access: true, ...full details... })
. - Test case: Metric ID not found -> returns
Err
. - Test case: DB error during permission check -> returns
Err
. - Test case: DB error during full data fetch (when permitted) -> returns
Err
.
- Integration Tests (
get_dashboard_handler
):- Setup: User, Dashboard, Metric A (user has CanView), Metric B (user has no permission), Metric C (ID in dashboard config but doesn't exist).
- Execute
get_dashboard_handler
. - Verify:
- Response status is OK.
response.metrics
map contains keys for Metric A and Metric B.response.metrics[&MetricA_ID]
hashas_access: true
and full details.response.metrics[&MetricB_ID]
hashas_access: false
and minimal details (id, name).response.metrics
does not contain a key for Metric C.- An error related to Metric C failing to load should be logged.
- Test variations with different user roles and public dashboard access.
8. Rollback Plan
- Revert changes to the modified handler and type files.
9. Dependencies
- Completion of Refactor Sharing Permission Helper.