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

158 lines
8.0 KiB
Markdown
Raw Normal View History

2025-04-08 23:20:20 +08:00
# 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<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).