insightfy-bloom-ms-ans / ACCESS_CONTROL_UPDATE.md
MukeshKapoor25's picture
feat(auth): Implement permission-based access control for widget endpoints
8bd2a47

Access Control Update Summary

Issue

The chart widget endpoints were using role-based checks (role: "admin") but the JWT token contains role_id: "admin". This caused a 403 error even for valid admin users.

Solution

Updated the access control system to use permission-based authentication instead of role-based checks, following the same pattern as TMS (Transaction Management Service).

Changes Made

1. Updated AccessID Enum (app/dependencies/auth.py)

Added new permission identifiers for widgets:

class AccessID(Enum):
    # ... existing permissions ...
    
    # Chart Widgets
    VIEW_CHART_WIDGETS = "view_charts"
    VIEW_REVENUE_CHARTS = "view_charts"
    VIEW_MARGIN_CHARTS = "view_charts"
    VIEW_INVENTORY_CHARTS = "view_charts"
    VIEW_SALES_CHARTS = "view_charts"
    VIEW_STAFF_CHARTS = "view_charts"
    VIEW_PERSONAL_CHARTS = "view_charts"
    
    # Table Widgets
    VIEW_TABLE_WIDGETS = "view_tables"
    VIEW_ORDER_TABLES = "view_tables"
    VIEW_INVENTORY_TABLES = "view_tables"
    VIEW_CUSTOMER_TABLES = "view_tables"
    
    # KPI Widgets
    VIEW_KPI_WIDGETS = "view_kpis"

2. Updated Chart Widget Router (app/routers/chart_widget_router.py)

Before (Role-based):

async def require_manager_or_admin(current_user: dict = Depends(get_current_user)):
    """Require manager or admin role."""
    if current_user.get("role") not in ["admin", "manager"]:
        raise HTTPException(status_code=403, detail="Manager or admin access required")
    return current_user

@router.post("/revenue-trend")
async def get_revenue_trend_chart(
    current_user: dict = Depends(require_manager_or_admin)
):
    ...

After (Permission-based):

async def require_view_charts_permission(current_user: dict = Depends(get_current_user)):
    """Require permission to view chart widgets."""
    return await require_permission(AccessID.VIEW_CHART_WIDGETS.value, current_user)

@router.post("/revenue-trend")
async def get_revenue_trend_chart(
    current_user: dict = Depends(require_view_charts_permission)
):
    ...

3. Updated All Chart Endpoints

All 9 chart widget endpoints now use permission-based access:

Endpoint Old Access New Access
/revenue-trend Admin, Manager roles view_charts permission
/gross-margin-trend Admin role view_charts permission
/channel-mix Admin, Manager roles view_charts permission
/top-skus Admin, Manager roles view_charts permission
/inventory-status All roles view_charts permission
/top-selling-products Manager, Associate roles view_charts permission
/staff-performance Manager role view_charts permission
/my-sales-trend Associate role view_personal_charts permission
/my-top-products Associate role view_personal_charts permission

4. Simplified Universal Endpoint

The /widget endpoint now:

  • Uses permission-based access control
  • Removed role checking logic
  • Simplified handler mapping

How It Works

Permission Check Flow

  1. User makes request with JWT token containing:

    {
      "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
      "associate_id": "AST011",
      "role_id": "admin",
      "branch_id": "hq"
    }
    
  2. Permission dependency calls require_permission():

    await require_permission("view_charts", current_user)
    
  3. MongoDB lookup checks access_roles collection:

    {
      "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
      "role_id": "admin",
      "permissions": {
        "charts": ["view", "create", "update"]
      }
    }
    
  4. Permission granted if role has the required permission

Required MongoDB Structure

The access_roles collection should have documents like:

{
  "_id": ObjectId("..."),
  "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
  "role_id": "admin",
  "permissions": {
    "charts": ["view", "create", "update", "delete"],
    "tables": ["view", "create", "update", "delete"],
    "kpis": ["view"],
    "analytics": ["view", "create"],
    "reports": ["view", "create", "update"]
  }
}

{
  "_id": ObjectId("..."),
  "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
  "role_id": "manager",
  "permissions": {
    "charts": ["view"],
    "tables": ["view"],
    "kpis": ["view"],
    "orders": ["view", "create", "update"]
  }
}

{
  "_id": ObjectId("..."),
  "merchant_id": "IN-NATUR-CHEANN-7D2B-O9BP1",
  "role_id": "associate",
  "permissions": {
    "charts": ["view"],
    "tables": ["view"],
    "sales": ["view", "create"]
  }
}

Benefits

  1. Flexible Access Control: Permissions can be configured per merchant and role
  2. Consistent with TMS: Uses the same permission system as other services
  3. Database-Driven: Permissions stored in MongoDB, easy to update
  4. Fine-Grained: Can control access at resource and action level
  5. Scalable: Easy to add new permissions without code changes

Testing

Test with cURL

curl -X 'POST' \
  'https://your-domain/api/v1/charts/revenue-trend?use_cache=true' \
  -H 'Authorization: Bearer YOUR_JWT_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "widget_id": "wid_revenue_trend_12m_001",
    "months": 12,
    "chart_type": "line"
  }'

Expected Response

Success (200):

{
  "success": true,
  "message": "Revenue trend chart retrieved successfully",
  "data": {
    "widget_id": "wid_revenue_trend_12m_001",
    "type": "chart",
    "title": "Revenue Trend (12 Months)",
    ...
  }
}

Permission Denied (403):

{
  "detail": "Forbidden"
}

Migration Steps

For Existing Deployments

  1. Create access_roles collection in MongoDB
  2. Add permission documents for each role
  3. Deploy updated code
  4. Test with different roles

Sample Permission Setup Script

// MongoDB script to set up permissions
db.access_roles.insertMany([
  {
    merchant_id: "IN-NATUR-CHEANN-7D2B-O9BP1",
    role_id: "admin",
    permissions: {
      charts: ["view", "create", "update", "delete"],
      tables: ["view", "create", "update", "delete"],
      kpis: ["view"],
      analytics: ["view", "create", "update"],
      reports: ["view", "create", "update", "delete"],
      dashboard: ["view", "create", "update", "delete"]
    }
  },
  {
    merchant_id: "IN-NATUR-CHEANN-7D2B-O9BP1",
    role_id: "manager",
    permissions: {
      charts: ["view"],
      tables: ["view"],
      kpis: ["view"],
      analytics: ["view"],
      reports: ["view", "create"]
    }
  },
  {
    merchant_id: "IN-NATUR-CHEANN-7D2B-O9BP1",
    role_id: "associate",
    permissions: {
      charts: ["view"],
      tables: ["view"]
    }
  }
]);

Files Modified

  1. βœ… app/dependencies/auth.py - Added widget permissions to AccessID enum
  2. βœ… app/routers/chart_widget_router.py - Updated all endpoints to use permission-based access

Verification

βœ… No syntax errors βœ… All endpoints updated βœ… Permission dependencies implemented βœ… Consistent with TMS pattern βœ… Ready for testing

Next Steps

  1. Set up access_roles collection in MongoDB
  2. Add permission documents for all roles
  3. Test with different user roles
  4. Update table_widget_router.py similarly
  5. Update kpi_widget_router.py similarly

Summary

Successfully migrated from role-based to permission-based access control, fixing the 403 error and providing a more flexible, database-driven permission system that's consistent with other services.