9.5 KiB
Sub-PRD: Refactor Sharing Permission Helper
Author: AI Assistant (Pair Programming with User) Date: 2023-10-27 Status: Proposed Parent PRD: Project: Granular Asset Permission Checks
1. Overview
This PRD details the creation or enhancement of a centralized helper function within the libs/sharing
crate. This function will encapsulate the logic for checking if a given user has at least one of a set of required permission roles for a specific asset (identified by ID and type). This promotes consistency and simplifies permission checking in various handlers, including those retrieving asset metadata (like get_metric_handler
, get_collection_handler
) and those executing actions based on assets (like the handler running metric SQL queries).
2. Problem Statement
Currently, permission checking logic might be duplicated or slightly varied across different handlers (e.g., get_collection_handler
, get_dashboard_handler
, get_metric_handler
, and potentially data execution handlers). Checking permissions for assets contained within other assets, or before executing an action related to an asset, requires a standardized, testable approach that considers direct user permissions, organization roles, and potentially public access settings of the specific asset.
3. Goals
- Create a reusable, modular, and easily testable function
check_specific_asset_access
withinlibs/sharing
. - This function should accept user context, asset details (ID, type, org ID), and required permission levels.
- It should return
Ok(true)
if the user meets the requirements,Ok(false)
if they don't, andErr
only for database or unexpected errors. - Consolidate permission checking logic for specific assets into this single helper, callable from multiple contexts (metadata retrieval, action execution).
4. Non-Goals
- Modifying the underlying permission tables (
asset_permissions
,users_to_organizations
). - Implementing permission checks for containers (collections/dashboards themselves) - this focuses on contained assets.
- Handling public access checks for the container object (though it might check public access for the target asset if relevant in the future).
5. Technical Design
- Location:
libs/sharing/src/permissions.rs
(create if not exists) orlibs/sharing/src/lib.rs
. - Function Signature:
// libs/sharing/src/permissions.rs use anyhow::Result; use diesel_async::AsyncPgConnection; use uuid::Uuid; use middleware::AuthenticatedUser; use database::enums::{AssetPermissionRole, AssetType}; use database::schema::{asset_permissions, users_to_organizations}; use diesel::{prelude::*, dsl::exists}; /// Checks if a user has the required permission level for a specific asset. /// /// This function checks: /// 1. Direct user permissions in `asset_permissions`. /// 2. The user's role within the asset's organization via `users_to_organizations`. /// /// Returns `Ok(true)` if the user has at least one of the `required_roles`, /// `Ok(false)` otherwise. Returns `Err` on database query failures. pub async fn check_specific_asset_access( conn: &mut AsyncPgConnection, user: &AuthenticatedUser, asset_id: &Uuid, asset_type: AssetType, asset_organization_id: Uuid, required_roles: &[AssetPermissionRole], ) -> Result<bool> { // --- 1. Check High-Level Organization Permissions First --- // Check if the user is WorkspaceAdmin or DataAdmin in the asset's organization let high_level_org_roles = [ database::enums::UserOrganizationRole::WorkspaceAdmin, database::enums::UserOrganizationRole::DataAdmin, ]; let has_high_level_org_role = select(exists( users_to_organizations::table .filter(users_to_organizations::user_id.eq(&user.id)) .filter(users_to_organizations::organization_id.eq(asset_organization_id)) .filter(users_to_organizations::deleted_at.is_null()) .filter(users_to_organizations::status.eq(database::enums::UserOrganizationStatus::Active)) .filter(users_to_organizations::role.eq_any(high_level_org_roles)) )) .get_result::<bool>(conn) .await; match has_high_level_org_role { Ok(true) => return Ok(true), // User is Org Admin/Data Admin, grant access Ok(false) => { /* Continue to check direct permissions */ } Err(e) => { tracing::error!( "DB error checking high-level organization permissions for user {} in org {}: {}", user.id, asset_organization_id, e ); return Err(anyhow!("Failed to check organization permissions: {}", e)); } } // --- 2. Check Direct Permissions --- let direct_permission_exists = select(exists( asset_permissions::table .filter(asset_permissions::identity_id.eq(&user.id)) .filter(asset_permissions::asset_id.eq(asset_id)) .filter(asset_permissions::asset_type.eq(asset_type)) .filter(asset_permissions::identity_type.eq(database::enums::IdentityType::User)) .filter(asset_permissions::deleted_at.is_null()) .filter(asset_permissions::role.eq_any(required_roles)), )) .get_result::<bool>(conn) .await; match direct_permission_exists { Ok(true) => return Ok(true), // Found sufficient direct permission Ok(false) => { /* Continue to check org permissions */ } Err(e) => { tracing::error!( "DB error checking direct asset permissions for asset {} type {:?}: {}", asset_id, asset_type, e ); // Consider returning Err only for non-NotFound errors if needed return Err(anyhow!("Failed to check direct asset permissions: {}", e)); } } // --- 3. Check Other Organization Permissions (e.g., Member role granting CanView) --- // If specific roles like Member should grant CanView, add that check here. // This check is only relevant if high-level admin check and direct permission check failed. // Example (If Member grants CanView): /* if required_roles.contains(&AssetPermissionRole::CanView) { let is_org_member = select(exists( users_to_organizations::table .filter(users_to_organizations::user_id.eq(&user.id)) .filter(users_to_organizations::organization_id.eq(asset_organization_id)) .filter(users_to_organizations::deleted_at.is_null()) .filter(users_to_organizations::status.eq(database::enums::UserOrganizationStatus::Active)) .filter(users_to_organizations::role.eq(database::enums::UserOrganizationRole::Member)) // Check for Member )) .get_result::<bool>(conn) .await; match is_org_member { Ok(true) => return Ok(true), // Org Member grants CanView Ok(false) => { /* Continue */ } Err(e) => { tracing::error!( "DB error checking Member organization permissions for asset {} type {:?} in org {}: {}", asset_id, asset_type, asset_organization_id, e ); // Don't return Err here, as failure to check Member role shouldn't block access // if other permissions might exist (though they were already checked). // Fall through to return false. } } } */ // If none of the above checks granted access, return false Ok(false) }
- File Changes:
- Create/Modify:
libs/sharing/src/permissions.rs
- Modify:
libs/sharing/src/lib.rs
(to export the function if needed)
- Create/Modify:
6. Implementation Plan
- Create
permissions.rs
if it doesn't exist. - Implement the
check_specific_asset_access
function as defined above. - Add comprehensive unit tests.
- Export the function from the
libs/sharing
crate.
7. Testing Strategy
- Unit Tests:
- Mock
AuthenticatedUser
andAsyncPgConnection
. - Test cases:
- User has direct
CanView
permission -> returnsOk(true)
whenCanView
is required. - User has direct
Owner
permission -> returnsOk(true)
whenCanView
orOwner
is required. - User has direct
CanView
permission -> returnsOk(false)
whenOwner
is required. - User has no direct permission but Org Admin role -> returns
Ok(true)
whenCanView
is required. - User has no direct permission and Org Member role -> returns
Ok(false)
(unless Member role check is added and CanView is required). - User has no relevant direct or org permission -> returns
Ok(false)
. - Database error during direct permission check -> returns
Err
. - Database error during org permission check -> returns
Err
. - User deleted from org -> returns
Ok(false)
. - Asset permission deleted -> returns
Ok(false)
.
- User has direct
- Mock
8. Rollback Plan
- Revert the changes to
libs/sharing
. Dependent PRs cannot be merged without this helper.
9. Dependencies
- None (this is the foundational piece).