# 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](mdc:prds/active/project_granular_asset_permissions.md) ## 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. ## 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`). Checking permissions for assets *contained within* other assets requires a standardized approach that considers direct user permissions, organization roles, and potentially public access settings of the specific asset. ## 3. Goals - Create a reusable function `check_specific_asset_access` within `libs/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, and `Err` only for database or unexpected errors. - Consolidate permission checking logic for specific assets into this helper. ## 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 1. **Location:** `libs/sharing/src/permissions.rs` (create if not exists) or `libs/sharing/src/lib.rs`. 2. **Function Signature:** ```rust // 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 { // --- 1. 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::(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)); } } // --- 2. Check Organization Permissions --- // Check if the user is part of the asset's organization AND has an Admin/Owner role // (Adjust roles based on specific requirements - maybe Members also get view?) let org_roles_to_check = [ database::enums::UserOrganizationRole::Admin, database::enums::UserOrganizationRole::Owner, // Add database::enums::UserOrganizationRole::Member if members should have view access ]; let sufficient_org_role_exists = 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(org_roles_to_check)) // We implicitly assume org roles grant at least 'CanView' equivalent. // If finer control is needed, we might need a mapping from OrgRole -> AssetPermissionRole. // For now, if any required_role is <= CanView and user has sufficient org role, grant access. // This simplifies logic: if user needs CanView/Edit/FullAccess/Owner and is Org Admin/Owner, they likely have it. )) // Only check org permissions if required_roles includes something an org admin might have (e.g., CanView) .filter(required_roles.iter().any(|r| *r <= AssetPermissionRole::CanView)) .get_result::(conn) .await; match sufficient_org_role_exists { Ok(true) => Ok(true), // Found sufficient org permission Ok(false) => Ok(false), // No sufficient direct or org permission found Err(e) => { tracing::error!( "DB error checking organization permissions for asset {} type {:?} in org {}: {}", asset_id, asset_type, asset_organization_id, e ); Err(anyhow!("Failed to check organization asset permissions: {}", e)) } } } ``` 3. **File Changes:** - Create/Modify: `libs/sharing/src/permissions.rs` - Modify: `libs/sharing/src/lib.rs` (to export the function if needed) ## 6. Implementation Plan 1. Create `permissions.rs` if it doesn't exist. 2. Implement the `check_specific_asset_access` function as defined above. 3. Add comprehensive unit tests. 4. Export the function from the `libs/sharing` crate. ## 7. Testing Strategy - **Unit Tests:** - Mock `AuthenticatedUser` and `AsyncPgConnection`. - Test cases: - User has direct `CanView` permission -> returns `Ok(true)` when `CanView` is required. - User has direct `Owner` permission -> returns `Ok(true)` when `CanView` or `Owner` is required. - User has direct `CanView` permission -> returns `Ok(false)` when `Owner` is required. - User has no direct permission but Org Admin role -> returns `Ok(true)` when `CanView` is required. - User has no direct permission and Org Member role -> returns `Ok(false)` (unless Member is added to `org_roles_to_check`). - 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)`. ## 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).