buster/api/prds/active/api_permission_field_fix.md

12 KiB

title author date status parent_prd ticket
Permission Field Fix Claude 2024-04-07 Draft project_bug_fixes_and_testing.md BUS-1063

Permission Field Fix

Problem Statement

The permission field in asset responses is inconsistent and sometimes missing required information. This causes issues with permission checking and display in the UI. The current implementation has several issues:

Current behavior:

  • Permission field sometimes missing from responses
  • Inconsistent permission field format
  • Missing role information
  • Incorrect inherited permissions
  • Lack of comprehensive testing for permission scenarios

Expected behavior:

  • Consistent permission field presence
  • Standardized permission format
  • Complete role information
  • Proper inheritance handling
  • Comprehensive test coverage using new test infrastructure

Impact:

  • User Impact: Incorrect permission display and access control
  • System Impact: Inconsistent permission state
  • Testing Impact: Missing edge cases and inheritance scenarios

Goals

  1. Fix permission field consistency
  2. Standardize permission format
  3. Add complete role information
  4. Fix permission inheritance
  5. Implement comprehensive tests using new test infrastructure

Non-Goals

  1. Adding new permission types
  2. Changing permission model
  3. Modifying permission UI
  4. Adding new permission features

Technical Design

Overview

The fix involves standardizing the permission field format and ensuring consistent inclusion in responses, with comprehensive testing using the new test infrastructure.

Permission Field Structure

// libs/models/src/permissions.rs

#[derive(Debug, Serialize, Deserialize)]
pub struct PermissionField {
    pub role: AssetPermissionRole,
    pub inherited: bool,
    pub inherited_from: Option<Uuid>,
    pub granted_at: DateTime<Utc>,
    pub granted_by: Option<Uuid>,
}

impl PermissionField {
    pub fn new(
        role: AssetPermissionRole,
        inherited: bool,
        inherited_from: Option<Uuid>,
        granted_by: Option<Uuid>,
    ) -> Self {
        Self {
            role,
            inherited,
            inherited_from,
            granted_at: Utc::now(),
            granted_by,
        }
    }
    
    pub fn from_permission(permission: &Permission) -> Self {
        Self {
            role: permission.role,
            inherited: permission.inherited,
            inherited_from: permission.inherited_from,
            granted_at: permission.granted_at,
            granted_by: permission.granted_by,
        }
    }
}

Asset Response Update

// libs/models/src/asset.rs

#[derive(Debug, Serialize)]
pub struct AssetResponse {
    pub id: Uuid,
    pub name: String,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub permission: PermissionField,  // Now required
    // ... other fields ...
}

impl Asset {
    pub async fn to_response(
        &self,
        user_id: Uuid,
        conn: &mut PgConnection
    ) -> Result<AssetResponse> {
        // Get user's permission
        let permission = Permission::find_for_user(
            self.id,
            user_id,
            conn
        ).await?;
        
        Ok(AssetResponse {
            id: self.id,
            name: self.name.clone(),
            created_at: self.created_at,
            updated_at: self.updated_at,
            permission: PermissionField::from_permission(&permission),
            // ... other fields ...
        })
    }
}

Test Cases

// libs/models/tests/permission_field_test.rs

use database::tests::common::{TestDb, TestSetup};
use database::tests::common::permissions::PermissionTestHelpers;
use database::tests::common::assets::AssetTestHelpers;

#[tokio::test]
async fn test_permission_field_direct() -> Result<()> {
    // Create test setup with admin user
    let setup = TestSetup::new(Some(UserOrganizationRole::Admin)).await?;
    
    // Create test asset using asset helpers
    let asset_id = AssetTestHelpers::create_test_asset(
        &setup.db,
        "Test Asset"
    ).await?;
    
    // Add direct permission using permission helpers
    PermissionTestHelpers::create_permission(
        &setup.db,
        asset_id,
        AssetPermissionRole::Owner
    ).await?;
    
    // Get asset response using test db connection
    let mut conn = setup.db.diesel_conn().await?;
    let asset = Asset::find_by_id(asset_id).await?;
    let response = asset.to_response(
        setup.user.id,
        &mut conn
    ).await?;
    
    // Verify permission field
    assert_eq!(response.permission.role, AssetPermissionRole::Owner);
    assert!(!response.permission.inherited);
    assert!(response.permission.inherited_from.is_none());
    assert_eq!(
        response.permission.granted_by,
        Some(setup.user.id)
    );
    
    Ok(())
}

#[tokio::test]
async fn test_permission_field_inherited() -> Result<()> {
    // Create test setup with admin user
    let setup = TestSetup::new(Some(UserOrganizationRole::Admin)).await?;
    
    // Create parent and child assets using asset helpers
    let parent_id = AssetTestHelpers::create_test_asset(
        &setup.db,
        "Parent Asset"
    ).await?;
    
    let child_id = AssetTestHelpers::create_test_asset(
        &setup.db,
        "Child Asset"
    ).await?;
    
    // Add parent permission using permission helpers
    PermissionTestHelpers::create_permission(
        &setup.db,
        parent_id,
        AssetPermissionRole::Owner
    ).await?;
    
    // Set inheritance using test db connection
    let mut conn = setup.db.diesel_conn().await?;
    diesel::update(assets::table)
        .filter(assets::id.eq(child_id))
        .set(assets::parent_id.eq(parent_id))
        .execute(&mut conn)
        .await?;
    
    // Get child asset response
    let child = Asset::find_by_id(child_id).await?;
    let response = child.to_response(
        setup.user.id,
        &mut conn
    ).await?;
    
    // Verify inherited permission field
    assert_eq!(response.permission.role, AssetPermissionRole::Owner);
    assert!(response.permission.inherited);
    assert_eq!(response.permission.inherited_from, Some(parent_id));
    
    Ok(())
}

#[tokio::test]
async fn test_permission_field_missing() -> Result<()> {
    // Create test setup with viewer role
    let setup = TestSetup::new(Some(UserOrganizationRole::Viewer)).await?;
    
    // Create asset without permission using asset helpers
    let asset_id = AssetTestHelpers::create_test_asset(
        &setup.db,
        "Test Asset"
    ).await?;
    
    // Try to get asset response using test db connection
    let mut conn = setup.db.diesel_conn().await?;
    let asset = Asset::find_by_id(asset_id).await?;
    let result = asset.to_response(
        setup.user.id,
        &mut conn
    ).await;
    
    assert!(result.is_err());
    
    // Verify error is logged using test db connection
    let error_log = error_logs::table
        .filter(error_logs::asset_id.eq(asset_id))
        .first::<ErrorLog>(&mut conn)
        .await?;
        
    assert_eq!(error_log.error_type, "PermissionNotFound");
    
    Ok(())
}

#[tokio::test]
async fn test_permission_field_edge_cases() -> Result<()> {
    // Create test setup with admin user
    let setup = TestSetup::new(Some(UserOrganizationRole::Admin)).await?;
    
    // Create test assets using asset helpers
    let asset_id = AssetTestHelpers::create_test_asset(
        &setup.db,
        "Test Asset"
    ).await?;
    
    // Test edge cases
    
    // Case 1: Multiple inheritance levels
    let parent_id = AssetTestHelpers::create_test_asset(
        &setup.db,
        "Parent Asset"
    ).await?;
    
    let grandparent_id = AssetTestHelpers::create_test_asset(
        &setup.db,
        "Grandparent Asset"
    ).await?;
    
    // Set up inheritance chain
    let mut conn = setup.db.diesel_conn().await?;
    diesel::update(assets::table)
        .filter(assets::id.eq(asset_id))
        .set(assets::parent_id.eq(parent_id))
        .execute(&mut conn)
        .await?;
        
    diesel::update(assets::table)
        .filter(assets::id.eq(parent_id))
        .set(assets::parent_id.eq(grandparent_id))
        .execute(&mut conn)
        .await?;
    
    // Add permission to grandparent
    PermissionTestHelpers::create_permission(
        &setup.db,
        grandparent_id,
        AssetPermissionRole::Owner
    ).await?;
    
    // Verify permission inheritance through chain
    let asset = Asset::find_by_id(asset_id).await?;
    let response = asset.to_response(
        setup.user.id,
        &mut conn
    ).await?;
    
    assert_eq!(response.permission.role, AssetPermissionRole::Owner);
    assert!(response.permission.inherited);
    assert_eq!(response.permission.inherited_from, Some(grandparent_id));
    
    // Case 2: Permission overrides
    
    // Add direct permission that should override inherited
    PermissionTestHelpers::create_permission(
        &setup.db,
        asset_id,
        AssetPermissionRole::CanEdit
    ).await?;
    
    let response = asset.to_response(
        setup.user.id,
        &mut conn
    ).await?;
    
    // Direct permission should take precedence
    assert_eq!(response.permission.role, AssetPermissionRole::CanEdit);
    assert!(!response.permission.inherited);
    assert!(response.permission.inherited_from.is_none());
    
    // Case 3: Circular inheritance
    
    // Create circular reference
    diesel::update(assets::table)
        .filter(assets::id.eq(grandparent_id))
        .set(assets::parent_id.eq(asset_id))
        .execute(&mut conn)
        .await?;
        
    // Should handle circular reference gracefully
    let result = asset.to_response(
        setup.user.id,
        &mut conn
    ).await;
    
    assert!(result.is_ok());
    
    Ok(())
}

Dependencies

  1. Test infrastructure from Test Infrastructure Setup
    • TestDb for database connections
    • TestSetup for user/org creation
    • PermissionTestHelpers for permission management
    • AssetTestHelpers for asset creation
  2. Existing permission models
  3. Database schema
  4. Error logging system

Implementation Plan

Phase 1: Fix Implementation

  1. Update permission field structure
  2. Standardize response format
  3. Fix inheritance logic
  4. Add error handling

Phase 2: Testing

  1. Implement test cases using new test infrastructure:
    • Direct permission tests
    • Inheritance tests
    • Missing permission tests
    • Edge case tests
  2. Verify test isolation using TestDb
  3. Validate cleanup functionality

Testing Strategy

Unit Tests

  1. Permission Field Tests

    • Test field structure
    • Verify serialization
    • Check field validation
  2. Permission Inheritance Tests

    • Test direct permissions
    • Test inherited permissions
    • Test permission overrides
    • Test inheritance chain
  3. Edge Case Tests

    • Test missing permissions
    • Test circular inheritance
    • Test multiple inheritance levels
    • Test concurrent modifications

Integration Tests

  1. API Tests

    • Test permission field in responses
    • Verify error handling
    • Check inheritance behavior
  2. Workflow Tests

    • Test permission changes
    • Verify inheritance updates
    • Test with asset operations

Test Data Management

Using the new test infrastructure:

  • Unique test IDs for isolation
  • Automatic cleanup after tests
  • Standardized test data creation

Success Criteria

  1. All tests passing using new infrastructure
  2. Permission fields consistent in responses
  3. Inheritance working correctly
  4. Edge cases handled properly
  5. Documentation updated

Security Considerations

  1. Permission Validation

    • Risk: Incorrect permission assignment
    • Mitigation: Comprehensive permission tests
    • Testing: Role-based access tests
  2. Inheritance Security

    • Risk: Unintended permission escalation
    • Mitigation: Strict inheritance rules
    • Testing: Inheritance chain validation

References

Appendix

  • libs/models/src/permissions.rs
  • libs/models/src/asset.rs
  • libs/models/tests/permission_field_test.rs
  • libs/handlers/src/assets/*.rs

Permission Field Reference

Required fields:

  • role: The user's role for the asset
  • inherited: Whether the permission is inherited
  • inherited_from: Parent asset ID if inherited
  • granted_at: When the permission was granted
  • granted_by: User ID who granted the permission