Enforce Permissions Step by Step¶
This tutorial walks through building and validating permissions for a GeneralManager. It uses the explicit additive AdditiveManagerPermission class and the reusable checks from the permission_checks registry.
1. Model the access rules¶
Start by encoding who may create, read, update, or delete each attribute. AdditiveManagerPermission exposes class attributes (__read__, __create__, __update__, __delete__) plus per-field overrides. Every string in these lists maps to a registered permission function.
from general_manager.manager import GeneralManager
from general_manager.permission.manager_based_permission import AdditiveManagerPermission
class Project(GeneralManager):
creator_id: int
status: str
sensitive_note: str
class Permission(AdditiveManagerPermission):
__read__ = ["isAuthenticated"]
__create__ = ["isAuthenticated"]
__update__ = ["isSelf", "inGroup:project_admins"]
__delete__ = ["isAdmin"]
sensitive_note = {
"read": ["inGroup:project_admins"],
"update": ["inGroup:project_admins"],
}
- Default lists apply to every attribute.
- Attribute overrides restrict specific fields without impacting the rest.
- Checks such as
isAuthenticated,isSelf, andinGroupare registered in thepermission_checksregistry.
If you want project-wide defaults for permission classes that omit these lists, set:
GENERAL_MANAGER = {
"DEFAULT_PERMISSIONS": {
"READ": ["public"],
"CREATE": ["isAuthenticated"],
"UPDATE": ["isAuthenticated"],
"DELETE": ["isAuthenticated"],
}
}
When this setting is absent, AdditiveManagerPermission falls back to the values shown above.
2. Attach filters for queryset access¶
Read permissions do more than guard individual attribute access. The GraphQL API calls get_permission_filter to narrow the queryset before results are returned, then applies a final per-instance read check. Each permission function may provide a filter companion.
from general_manager.permission.permission_checks import register_permission
@register_permission(
"belongsToCustomer",
permission_filter=lambda user, config: {
"filter": {f"{config[0]}__owner_id": user.id}
}
if config
else None,
)
def can_access_customer(instance, user, config):
customer_field = config[0]
return getattr(instance, customer_field).owner_id == user.id
Add "belongsToCustomer:customer" to __read__ to produce filters automatically when the GraphQL layer runs the resolver. Use python -m pytest with fixtures that hit get_permission_filter() and list/search responses so both the prefilter and the final instance gate match expectations.
On production paths, these list/search checks also emit aggregate structured logs through the standard get_logger(..., context=...) pattern. That gives you candidate/authorized/denied counts plus reason labels such as unfilterable read rules or delegated __based_on__ fallbacks without logging one event per row.
3. Chain permissions with __based_on__¶
Complex domains often reuse another manager's permission logic. Setting __based_on__ delegates to that nested manager. The implementation documented under the manager-based permission classes validates the attribute, forwards CRUD checks, and merges queryset filters.
class ProjectDocument(GeneralManager):
project: Project
file_path: str
class Permission(AdditiveManagerPermission):
__based_on__ = "project"
__create__ = ["isAuthenticated"]
file_path = {"read": ["isAuthenticated"], "update": ["isSelf"]}
When a user fails a delegated check, the action is denied immediately. Filters returned from Project.Permission.get_permission_filter() are namespaced as {"filter": {"project__...": ...}}, keeping queryset logic consistent.
If you need a field-specific rule to replace the class-level CRUD rule instead of adding an extra gate, switch to OverrideManagerPermission. __based_on__ still stays an outer gate in that mode.
If project is None at runtime, implicit CRUD rules on the current permission fall back to GENERAL_MANAGER["DEFAULT_PERMISSIONS"] (or to public for reads and isAuthenticated for writes when that setting is not configured). Explicitly declared CRUD lists still win.
4. Validate at runtime¶
BasePermission exposes helpers used by managers and mutations to enforce permissions. Call them directly in tests or custom workflows:
from general_manager.permission.base_permission import BasePermission
payload = {"status": "active"}
BasePermission.check_create_permission(payload, Project, request_user=user)
check_create_permission,check_update_permission, andcheck_delete_permissionraisePermissionCheckErrorwhen a rule fails.PermissionDataManagermerges the old and new state for update checks, making diff-based rules straightforward.
5. Capture audit trails¶
Every permission check may emit audit events when logging is enabled (see the audit logging tutorial for setup). The audit payload contains:
action:"create","read","update","delete", or"mutation"attributes: the fields evaluatedpermissions: the expressions considered, including those from__based_on__bypassed:Truewhen a superuser short-circuits the evaluation
Use these events in observability pipelines to verify that your permission rules fire as expected and to detect denied access attempts.
6. Recommended testing strategy¶
- Exercise happy-path scenarios where authorised users succeed.
- Attempt the same operations with unauthorised users and assert on the raised
PermissionCheckError. - For list endpoints, inspect the queryset returned by
get_permission_filter()and ensure it hides records belonging to other users. - If audit logging is enabled during tests, capture emitted events using a stub logger to assert on the recorded metadata.
With these steps, your permission classes stay in sync with business requirements while remaining transparent to reviewers and observability tooling.