Waldur Permission System Guide
Permission Factory Usage
ALWAYS use permission_factory
instead of manual has_permission
checks in ViewSets.
For ViewSet Actions
| # Define permissions as class attributes
compliance_overview_permissions = [
permission_factory(PermissionEnum.UPDATE_CALL)
]
@action(detail=True, methods=["get"])
def compliance_overview(self, request, uuid=None):
# No manual permission check needed - handled by permission_factory
pass
|
Permission Factory Patterns
- Current Object:
permission_factory(PermissionEnum.PERMISSION_NAME)
- no path needed
- Related Object:
permission_factory(PermissionEnum.PERMISSION_NAME, ["customer"])
- for related objects
- Nested Path:
permission_factory(PermissionEnum.PERMISSION_NAME, ["project.customer"])
- for nested relationships
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | # Use declarative permission attributes instead of manual perform_* overrides
def check_create_permissions(request, view, obj=None):
"""Check permissions for creating reviews."""
serializer = view.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
proposal = serializer.validated_data["proposal"]
if not has_permission(
request.user,
PermissionEnum.MANAGE_PROPOSAL_REVIEW,
proposal.round.call,
):
raise exceptions.PermissionDenied()
def check_destroy_permissions(request, view, obj=None):
"""Check permissions for destroying reviews."""
if obj and not has_permission(
request.user,
PermissionEnum.MANAGE_PROPOSAL_REVIEW,
obj.proposal.round.call,
):
raise exceptions.PermissionDenied()
create_permissions = [check_create_permissions]
destroy_permissions = [check_destroy_permissions]
|
When to Use Manual Checks
- Complex permission logic that doesn't map to standard object relationships
- Custom validation that requires dynamic permission targets
- Legacy code not yet refactored to declarative patterns
Permission System Behavior
Expiration Handling
- Basic permission queries (
get_users_with_permission
, get_scope_ids
) include all roles regardless of expiration
- Expiration checking is explicit via
has_user(expiration_time=False)
, not implicit in has_permission()
- Use
has_user(expiration_time=current_time)
for time-based validation
Error Handling
permission_factory
doesn't catch AttributeError
and convert to PermissionDenied
- Test for actual exceptions the system raises, not ideal ones
- Handle
AttributeError
when accessing missing nested attributes
Data Accuracy Critical Areas
- User counting: Always use
distinct()
on user_id to avoid double-counting users with multiple roles
- Permission checks: Handle edge cases (None scope, missing attributes) gracefully
- Financial calculations: Never approximate - exact calculations required
Query Optimization Strategy
- Use
select_related()
for foreign keys
- Use
prefetch_related()
for reverse relationships
- Use
distinct()
for deduplication instead of manual logic
- Accept 20-30 queries for complex operations rather than approximations
- Verify permission checks use reasonable query counts (≤3 for most operations)