Skip to content

Waldur Permission System Guide

Permission Factory Usage

ALWAYS use permission_factory instead of manual has_permission checks in ViewSets.

For ViewSet Actions

1
2
3
4
5
6
7
8
9
# 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

For perform_create/perform_destroy Methods

 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

Adding New Permissions

1. Define the Permission Enum

Add the new permission to PermissionEnum in src/waldur_core/permissions/enums.py:

1
2
class PermissionEnum(StrEnum):
    MY_NEW_PERMISSION = "RESOURCE.MY_ACTION"

If the permission is for managing team members (creating/updating/deleting roles) on a scope type, also add it to the CREATE_PERMISSIONS, UPDATE_PERMISSIONS, and DELETE_PERMISSIONS dicts in the same file.

2. Assign to Roles via permissions.yaml

Do NOT use data migrations to assign permissions to roles. Instead, add the permission to the appropriate roles in docker/rootfs/etc/waldur/permissions.yaml:

1
2
3
4
- role: CUSTOMER.OWNER
  scope: customer
  permissions:
    - RESOURCE.MY_ACTION   # Add here

This file is loaded by the import_roles management command, which runs on deployment. The command creates roles and syncs their permissions from the YAML definition.

3. Use in ViewSets

1
2
3
my_action_permissions = [
    permission_factory(PermissionEnum.MY_NEW_PERMISSION, ["project.customer"])
]

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

Performance Optimization

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)