Skip to content

Client Filtering

Overview

The Tracker REST API implements client filtering to ensure that users can only access data related to clients they have permission to view. This guide explains how client filtering works and how it's implemented in the API.

How Client Filtering Works

User Permissions

Each user in the system has a client_list array that contains the IDs of clients they have access to. Users with the admin role have unrestricted access to all data.

{
  "id": 1,
  "name": "John Doe",
  "email": "john.doe@example.com",
  "roles": ["user"],
  "client_list": [1, 2, 3]
}

In this example, the user has access to clients with IDs 1, 2, and 3.

Data Access Rules

The following rules apply to data access:

  1. Admin Users: Users with the admin role can access all data without any filtering.
  2. Regular Users: Users without the admin role can only access data related to clients in their client_list.
  3. Location Reports: Location reports have no client relationship and can only be accessed by admin users.
  4. User Management: Only admin users can create, read, update, or delete user accounts. Regular users can only access and update their own profile via the /users/me endpoint.

Client Relationships

Data is filtered based on its relationship to clients:

  • Trackers: A tracker belongs to a production run, which belongs to a brand, which belongs to a client.
  • Production Runs: A production run belongs to a brand, which belongs to a client.
  • Brands: A brand belongs directly to a client.
  • Location History: Location history belongs to a tracker, which follows the same relationship chain as trackers.
  • Location Reports: Raw location reports have no client relationship and are only accessible to admin users.

Implementation Details

Client Filter Utility

The client filtering is implemented using a utility function that applies the appropriate filters to database queries:

def apply_client_filter(query, model, user):
    """
    Apply client filtering to a query based on user's client_list.
    """
    # Special case for location_reports - only admins can access
    if model == models.LocationReport:
        if crud.user.is_admin(user):
            return query
        else:
            # Return empty result for non-admins
            return query.filter(False)

    # For other models, apply normal client filtering
    if crud.user.is_admin(user):
        return query  # No filtering for admins

    if not user.client_list:
        return query.filter(False)  # Empty result if no clients in list

    # Determine the relationship path to client_id based on model
    if model == models.Tracker:
        return query.join(models.Tracker.production_run)\
                   .join(models.ProductionRun.brand)\
                   .filter(models.Brand.client_id.in_(user.client_list))
    elif model == models.ProductionRun:
        return query.join(models.ProductionRun.brand)\
                   .filter(models.Brand.client_id.in_(user.client_list))
    elif model == models.Brand:
        return query.filter(models.Brand.client_id.in_(user.client_list))
    elif model == models.LocationHistory:
        return query.join(models.LocationHistory.tracker)\
                   .join(models.Tracker.production_run)\
                   .join(models.ProductionRun.brand)\
                   .filter(models.Brand.client_id.in_(user.client_list))

    return query  # Default case

Extended CRUD Base Class

The base CRUD class is extended to include methods that apply client filtering:

class CRUDBaseWithClientFilter(CRUDBase):
    def get_with_client_filter(self, db, *, id, user):
        """Get a single item by ID with client filtering applied."""
        query = db.query(self.model).filter(self.model.id == id)
        filtered_query = apply_client_filter(query, self.model, user)
        return filtered_query.first()

    def get_multi_with_client_filter(self, db, *, user, skip=0, limit=100):
        """Get multiple items with client filtering applied."""
        query = db.query(self.model)
        filtered_query = apply_client_filter(query, self.model, user)
        return filtered_query.offset(skip).limit(limit).all()

API Routes

API routes use the client filtering functionality to ensure users can only access data they have permission to view:

@router.get("/", response_model=List[schemas.Tracker])
def read_trackers(
    filtered_db: Tuple[Session, models.User] = Depends(deps.get_filtered_db),
    skip: int = 0,
    limit: int = 100,
    production_run_id: int = Query(None, description="Filter by production run ID"),
    status: TrackerStatus = Query(None, description="Filter by tracker status"),
) -> Any:
    """
    Retrieve trackers.

    Trackers are filtered based on the user's client_list.
    Admin users can see all trackers.
    """
    db, current_user = filtered_db

    # Create base query
    if production_run_id:
        query = db.query(models.Tracker).filter(
            models.Tracker.production_run_id == production_run_id
        )
    elif status:
        query = db.query(models.Tracker).filter(
            models.Tracker.current_status == status
        )
    else:
        query = db.query(models.Tracker)

    # Apply client filtering
    filtered_query = apply_client_filter(query, models.Tracker, current_user)

    # Apply pagination
    trackers = filtered_query.offset(skip).limit(limit).all()

    return trackers

Special Cases

Location Reports

Location reports are a special case because they have no client relationship. They can only be accessed by admin users:

@router.get("/", response_model=List[dict])
def read_location_reports(
    db: Session = Depends(deps.get_db),
    skip: int = 0,
    limit: int = 100,
    hashed_adv_key: Optional[str] = Query(None, description="Filter by hashed advertisement key"),
    current_user: models.User = Depends(deps.get_current_admin_user),  # Admin-only endpoint
) -> Any:
    """
    Retrieve raw location reports.

    This endpoint is restricted to admin users only.
    Location reports have no client relationship, so they can only be accessed by admins.
    """
    # ...

User Management

User management has special access rules:

@router.get("/{user_id}", response_model=schemas.User)
def read_user_by_id(
    user_id: int,
    current_user: models.User = Depends(deps.get_current_active_user),
    db: Session = Depends(deps.get_db),
) -> Any:
    """
    Get a specific user by id.

    Only admin users can view other users' profiles.
    Regular users can only access their own profile via the /me endpoint.
    """
    # Check if the user is trying to access their own profile
    if user_id == current_user.id:
        # Redirect them to use the /me endpoint instead
        raise HTTPException(
            status_code=403,
            detail="Please use the /users/me endpoint to access your own profile",
        )

    # Only admin users can view other users' profiles
    if not crud.user.is_admin(current_user):
        raise HTTPException(
            status_code=403,
            detail="Not enough permissions. Only admins can view user profiles."
        )

    # Get the requested user
    user = crud.user.get(db, id=user_id)
    if not user:
        raise HTTPException(
            status_code=404,
            detail="The user with this id does not exist in the system",
        )

    return user

Conclusion

Client filtering ensures that users can only access data related to clients they have permission to view. This is implemented at the database query level, ensuring efficient and consistent filtering across all API endpoints. Additionally, special access rules are in place for location reports and user management to further enhance security.