Role-based Access Control (RBAC)
Overview
Waldur implements a comprehensive Role-Based Access Control (RBAC) system that determines what actions users can perform within the platform. The authorization system consists of three core components:
- Permissions - Unique strings that designate specific actions (e.g.,
OFFERING.CREATE
,PROJECT.UPDATE
) - Roles - Named collections of permissions (e.g.,
CUSTOMER.OWNER
,PROJECT.ADMIN
) - User Roles - Assignments linking users to roles within specific scopes
This functionality is implemented in the waldur_core.permissions
application and provides fine-grained access control across all platform resources.
First thing to remember is to use PermissionEnum
to define permissions instead of using plain string or standalone named constant, otherwise they would not be pushed to frontend.
1 2 3 4 5 |
|
Next, let's assign that permissions to role.
1 2 3 4 5 6 |
|
Now, let's assign customer owner role to particular user and customer:
1 2 3 4 5 6 7 8 |
|
Finally, we can check whether user is allowed to create offering in particular organization.
1 2 3 4 |
|
Please note that this function accepts not only customer, but also project and offering as a scope. Consider these models as authorization aggregates. Other models, such as resources and orders, should refer to these aggregates to perform authorization check. For example:
1 |
|
Core Concepts
Authorization Scopes
Waldur supports multiple authorization scopes, each representing a different organizational level:
Scope Type | Model | Description |
---|---|---|
Customer | structure.Customer |
Organization-level permissions |
Project | structure.Project |
Project-level permissions within an organization |
Offering | marketplace.Offering |
Service offering permissions |
Service Provider | marketplace.ServiceProvider |
Provider-level permissions |
Call | proposal.Call |
Call for proposals permissions |
Proposal | proposal.Proposal |
Individual proposal permissions |
System Roles
The platform includes several predefined system roles:
Customer Roles
CUSTOMER.OWNER
- Full control over the organizationCUSTOMER.SUPPORT
- Support access to organization resourcesCUSTOMER.MANAGER
- Management capabilities for service providers
Project Roles
PROJECT.ADMIN
- Full project administrationPROJECT.MANAGER
- Project management capabilitiesPROJECT.MEMBER
- Basic project member access
Offering Roles
OFFERING.MANAGER
- Manage marketplace offerings
Call/Proposal Roles
CALL.REVIEWER
- Review proposals in callsCALL.MANAGER
- Manage calls for proposalsPROPOSAL.MEMBER
- Proposal team memberPROPOSAL.MANAGER
- Proposal management
Role Features
Time-based Roles
Roles can have expiration times, allowing for temporary access:
1 2 3 4 5 6 7 8 9 10 |
|
Role Revocation
Roles can be explicitly revoked before expiration:
1 2 3 4 5 6 7 8 |
|
Migration example
Previously we have relied on hard-coded roles, such as customer owner and project manager. Migration to dynamic roles on backend is relatively straightforward process. Consider the following example.
1 2 |
|
As you may see, we have relied on selectors with hard-coded roles. The main drawback of this approach is that it is very hard to inspect who can do what without reading all source code. And it is even hard to adjust this behaviour. Contrary to its name, by using dynamic roles we don't need to care much about roles though.
1 2 3 4 5 6 7 |
|
Here we use permission_factory
function which accepts permission string and list of paths to scopes, either customer, project or offering. It returns function which accepts request and raises an exception if user doesn't have specified permission in roles connected to current user and one of these scopes.
Permissions for viewing
Usually it is implemented filter backend, such as GenericRoleFilter
, which in turn uses get_connected_customers
and get_connected_projects
function because customer and project are two main permission aggregates.
1 2 3 4 5 6 |
|
Although this approach works fine for trivial use cases, often enough permission filtering logic is more involved and we implement get_queryset
method instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Permissions for object creation and update
Usually it is done in serializer's validate method.
1 2 3 4 5 6 7 8 9 10 |
|
Permission Checking Utilities
Core Functions
has_permission(request, permission, scope)
Checks if a user has a specific permission in a given scope:
1 2 3 4 5 6 7 |
|
Note: Staff users automatically pass all permission checks.
permission_factory(permission, sources=None)
Creates a permission checker function for use in ViewSets:
1 2 3 4 5 6 7 8 9 10 |
|
The sources
parameter specifies paths to traverse from the current object to find the scope.
User and Role Management
Getting Users with Roles
1 2 3 4 5 6 7 8 9 10 |
|
Managing User Roles
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 26 27 28 29 30 31 |
|
Filtering by Permissions
Using get_connected_customers
and get_connected_projects
These functions return all customers/projects where the user has any role:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Permission Categories
Offering Permissions
Permission | Description |
---|---|
OFFERING.CREATE |
Create new offerings |
OFFERING.UPDATE |
Update offering details |
OFFERING.DELETE |
Delete offerings |
OFFERING.PAUSE/UNPAUSE |
Control offering availability |
OFFERING.MANAGE_USER_GROUP |
Manage offering user groups |
Resource Permissions
Permission | Description |
---|---|
RESOURCE.TERMINATE |
Terminate resources |
RESOURCE.SET_USAGE |
Report resource usage |
RESOURCE.SET_LIMITS |
Update resource limits |
RESOURCE.SET_STATE |
Change resource state |
Order Permissions
Permission | Description |
---|---|
ORDER.LIST |
View orders |
ORDER.APPROVE |
Approve orders |
ORDER.REJECT |
Reject orders |
ORDER.CANCEL |
Cancel orders |
Project/Customer Permissions
Permission | Description |
---|---|
PROJECT.CREATE |
Create projects |
PROJECT.UPDATE |
Update project details |
PROJECT.DELETE |
Delete projects |
CUSTOMER.CREATE |
Create customers |
CUSTOMER.UPDATE |
Update customer details |
Best Practices
1. Always Use PermissionEnum
Define permissions in PermissionEnum
to ensure they're properly registered and available to the frontend:
1 2 3 4 5 6 |
|
2. Use Appropriate Scopes
Choose the right scope for permission checks:
1 2 3 4 5 6 7 8 |
|
3. Implement Proper Permission Chains
When checking permissions on nested resources, traverse to the appropriate scope:
1 2 3 4 5 |
|
4. Use Filter Backends for List Views
For list endpoints, use GenericRoleFilter
or implement custom filtering:
1 2 |
|
5. Audit Role Changes
Role changes are automatically logged via signals (role_granted
, role_updated
, role_revoked
) with enhanced context including initiator and reason. Always pass current_user
and optional reason
for clear audit trails:
1 2 3 4 5 6 7 |
|
Enhanced Logging Context
All role change logs now include:
initiated_by
: Shows either "System" (for automatic operations) or "User Name (username)" (for manual operations)reason
: Specific reason for the change, with automatic defaults:- Manual API operations:
"Manual role assignment/removal/update via API"
- Automatic expiration:
"Automatic expiration"
or"Automatic expiration cleanup task"
- Project deletion:
"Project deletion cascade"
- Scope changes:
"Project moved to different customer"
,"Offering moved to different provider"
Common Automatic Reasons
The system automatically assigns these reasons when not explicitly provided:
Scenario | Default Reason |
---|---|
API user operations with current_user |
"Manual [operation] via API" |
Expiration task | "Automatic expiration cleanup task" |
Project deletion | "Project deletion cascade" |
Role expiration detection | "Automatic expiration" |
System operations without current_user |
"System-initiated [operation]" |
6. Performance and Accuracy Guidelines
Exact User Counting
When counting users across roles, always use exact calculations to avoid double-counting users with multiple roles:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Query Optimization
Use Django ORM efficiently for permission-related queries:
1 2 3 4 5 6 7 8 |
|
Error Handling Best Practices
Handle edge cases gracefully in permission checking:
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 26 27 28 29 30 31 32 |
|
7. Time-based Role Best Practices
Default Parameter Behavior
The has_user
function has specific behavior for the expiration_time
parameter:
1 2 3 4 5 6 7 8 9 |
|
API Design Consistency
When designing permission-related APIs:
- Default parameters should match the most common use case
- Error types should be consistent:
AttributeError
for configuration/code errors (invalid attribute paths)PermissionDenied
for access control failuresValidationError
for user input errors
Testing and Debugging Permissions
Testing Permission Logic
When writing tests for permission functionality:
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 26 |
|
Performance Testing
Monitor query counts for permission-related operations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Debugging Permission Issues
When debugging permission problems:
- Check role assignments:
1 2 3 |
|
- Verify permission assignments:
1 2 3 4 |
|
- Test permission paths:
1 2 3 4 5 6 7 8 |
|
- Enable verbose logging:
1 2 |
|
Common Issues and Solutions
Issue: User count approximations
Problem: Double-counting users with multiple roles
Solution: Always use distinct()
on user_id when counting across multiple role assignments
Issue: Permission factory AttributeError
Problem: Invalid attribute paths in permission_factory sources
Solution: Verify object relationships and use try/catch for graceful error handling
Issue: Performance degradation in role filtering
Problem: N+1 queries when checking permissions for many objects
Solution: Use select_related()
and prefetch_related()
to optimize database queries
Issue: Time-based role confusion
Problem: Unclear behavior of has_user
with different expiration_time parameters
Solution: Understand the three modes:
expiration_time=False
(default): Any active roleexpiration_time=None
: Only permanent rolesexpiration_time=datetime
: Roles active at specific time