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:
- Admin Users: Users with the
adminrole can access all data without any filtering. - Regular Users: Users without the
adminrole can only access data related to clients in theirclient_list. - Location Reports: Location reports have no client relationship and can only be accessed by admin users.
- 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/meendpoint.
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.