buster/api/prds/active/refactor_sharing_permission...

8.0 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.

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:
    // 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 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));
            }
        }
    
        // --- 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::<bool>(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).