Map Django and VUEDA Permission Names
This guide covers the end-to-end flow for configuring VUEDA's Permission Mapping; the mechanism that translates between Django's built-in permission names (add, change, view) and VUEDA's CRUDL names (create, update, read). The mapping affects permission generation during migrations, runtime permission checks across all server layers, and the codenames stored in auth_permission rows. Getting this right at project setup prevents codename mismatches that surface as unexpected 403 responses or missing permissions.
The guide assumes familiarity with the permission model. If you have not read Permission Model, start there; it explains the layered evaluation that consumes these codenames. For the boundary between server authorization and client UI semantics, see Authorization vs UI Semantics.
Goal and Preconditions
The objective is a project where:
- Permission codenames in
auth_permissionmatch the names used by runtime permission checks. ObjectPermissionsmaps HTTP methods to the correct codenames for the chosen mapping direction.- Runtime paths that build codenames directly from
PERMISSION_NAMES_MAPPING(Model Info, history, workflow, Row-Level Permissions filtering) resolve consistently. - Tests exercise the mapped codenames and confirm expected allow/deny outcomes.
Before you begin, ensure the following are in place:
The project has a Django settings module that calls get_defaults from vueda.core.default_settings or otherwise defines PERMISSION_NAMES_MAPPING. The default mapping is {"add": "create", "change": "update", "view": "read"}; this is the standard VUEDA configuration and what VUEDA's own tests exercise. If you started from a VUEDA project template, this is already set up in your base settings (see templates/integrator-monorepo/server/config/settings/base.py.jinja for the reference implementation).
Choose Mapping Direction
PERMISSION_NAMES_MAPPING is a dictionary that maps Django's built-in action names to the codename strings used in auth_permission rows and runtime checks. The mapping runs through Django's get_permission_codename function, which VUEDA monkey-patches to apply the translation.
The default mapping translates Django's vocabulary to CRUDL:
| Django action | VUEDA codename | Example for myapp.Widget |
|---|---|---|
add | create | myapp.create_widget |
change | update | myapp.update_widget |
view | read | myapp.read_widget |
delete | (unchanged) | myapp.delete_widget |
delete is not in the mapping because the codename does not change.
list is an additional permission not part of the mapping; it is generated directly as list_*. VUEDA's base model meta declares list in default_permissions for VUEDA models. Additionally, patch_django patches Options.__init__ to inject list into default_permissions for any Django model that does not already declare it, including third-party models. This means every model in the project — including Django's own auth.Permission, auth.Group, and contenttypes.ContentType — has a list_* permission row after migration.
If your project needs to use the opposite mapping direction (such as mapping VUEDA names back to Django names for compatibility with third-party apps that expect add, change, or view), set PERMISSION_NAMES_MAPPING to match that need. Be aware that reverse mappings activate a code path in patch_django that rewrites the perms_map on ObjectPermissions. This ensures HTTP-method-to-codename resolution stays consistent with the mapping, but it also means the perms_map at runtime may differ from what the source code declares. DynamicObjectPermissions, and the WorkflowObjectPermissions class built on it, need no rewrite: they name the CRUDL action and resolve it through the mapping when the check runs. Validate explicitly if you use a non-default mapping.
The mapping must be decided before Django generates permission rows. Once migrate runs, the codenames in auth_permission are set. Changing the mapping after initial migration does not automatically update existing permission rows or group assignments.
Set Mapping and Patch Import Order
The patch_django module applies the mapping by monkey-patching two Django internals: auth.get_permission_codename (used at runtime to resolve codenames) and management._get_builtin_permissions (used during migration to generate permission rows). Both patched functions read PERMISSION_NAMES_MAPPING from settings at call time, not at import time; the value is cached and the cache clears on Django's setting_changed signal, so a mapping change made after patch_django is imported — including override_settings(PERMISSION_NAMES_MAPPING=...) in tests — still reaches codename resolution and permission-row generation.
patch_django also decides, once at import time, whether ObjectPermissions.perms_map needs a reverse-mapping rewrite. That decision is not re-evaluated later, so if you use a reverse mapping, PERMISSION_NAMES_MAPPING must still be finalized in settings before the patch module is imported.
If you started from a VUEDA project template, the patch import is already in your base settings:
# This needs to be imported after any customizations to the PERMISSION_NAMES_MAPPING, so
# all permission names can be mapped to the correct names before django starts using them.
from vueda.core import patch_django # noqa F401If you are setting up a project manually, place this import in your base settings module after PERMISSION_NAMES_MAPPING is defined. The import must execute at every runtime entry point that generates or uses permissions: the server process, the test runner, and the migration environment.
Import order matters for the reverse-mapping perms_map rewrite, since that decision is made once at patch_django import time. If you override PERMISSION_NAMES_MAPPING in an environment-specific settings file (for example, local.py importing from base.py via from config.settings.base import *) after the base module's patch import, ObjectPermissions.perms_map keeps whichever rewrite the base mapping selected. Finalize the reverse mapping before the first patch_django import. Codename resolution itself (get_permission_codename, get_builtin_permissions) and the runtime paths that read PERMISSION_NAMES_MAPPING directly (row-level filtering, model-info metadata, workflow state checks, the dynamic-model permission classes, and workflow state history access) are not affected by this ordering, since all of them read the current setting value at call time.
patch_django is not auto-imported by vueda.core. An explicit import is required. If the import is missing, no monkey-patch is applied: Django generates add_*/change_*/view_* codenames, but runtime checks look for create_*/update_*/read_* codenames, and every permission check fails.
Validate Generated Permission Codenames
After running migrations, verify that the generated codenames match expectations. The auth_permission table should contain rows with codenames in the mapped vocabulary.
For a model myapp.Widget with default VUEDA mapping, expect these codenames: create_widget, read_widget, update_widget, delete_widget, list_widget.
The get_builtin_permissions patch includes deduplication logic. If the mapping collapses two actions to the same codename (for example, mapping both list and view to the same name), the patch skips the duplicate to avoid a unique constraint violation during create_permissions. Verify that the expected number of permission rows exists for each model.
Validate Runtime Permission Checks
Several runtime paths build codenames directly from PERMISSION_NAMES_MAPPING rather than going through the DRF permission class. These paths must resolve to the same codenames as in auth_permission.
CRUDL HTTP-method checks. ObjectPermissions.perms_map maps HTTP methods to codename patterns. With the default mapping, GET resolves to list_* or read_*, POST to create_*, PUT/PATCH to update_*, DELETE to delete_*. With a reverse mapping, the patch rewrites perms_map entries to use the reversed names. Verify by making authenticated requests for each HTTP method and checking that the expected permission is required.
Row-level list filtering. ListRowLevelViewSetMixin.apply_row_level_filter builds the perm_type by extracting the action prefix from the full codename. The prefix must match the mapping's output.
Model-info choice and filter-choice endpoints. These endpoints check list and read permissions using codenames derived from the mapping. A user without the mapped read codename cannot access field choices; a user without the mapped list codename on a related model cannot access related-model choices.
Object history endpoint. The history view checks the mapped read codename before returning history data.
Workflow object-state endpoint. The workflow object_state view checks the mapped read codename for the target instance.
Verification Matrix
Validate the mapping end-to-end by running a permission matrix. For each HTTP method and endpoint type, test with a user who holds only one CRUDL codename at a time.
| Test scenario | Expected codename | Expected outcome |
|---|---|---|
GET list | list_* | 200 if held, 403 if not |
GET detail | read_* | 200 if held, 403 if not |
POST create | create_* | 201 if held, 403 if not |
PUT/PATCH update | update_* | 200 if held, 403 if not |
DELETE | delete_* | 204 if held, 403 if not |
| Field choices | read_* (source model) | 200 if held, 403 if not |
| Related-model choices | read_* + list_* (related) | 200 if both held, 403 if either missing |
| Filter choices | read_* (source model) | 200 if held, 403 if not |
| Object history | read_* | 200 if held, 403 if not |
| Workflow object state | read_* | 200 if held, 403 if not |
VUEDA's own test suite exercises the default CRUDL mapping. If you use a non-default mapping, add project-level tests that cover the above matrix with your mapped codenames.
Troubleshooting
Every permission check fails with 403. The patch_django import is missing. Runtime checks look for create_*/read_*/update_* codenames, but Django generated add_*/view_*/change_* codenames (or vice versa). Verify the import is present and executes before any permission check runs. Check all entrypoints: server, tests, and management commands.
Permissions work in the server but fail in tests. The test settings module does not import patch_django, or imports it before PERMISSION_NAMES_MAPPING is defined. Verify that the test settings follow the same import order as the server settings.
perms_map at runtime does not match the source code. If a reverse mapping is active, patch_django rewrites perms_map entries on ObjectPermissions at import time. The runtime perms_map reflects the patched values, not the values declared in the class definition. Inspect the runtime value with a debugger or print statement if the behaviour does not match expectations.
Changing the mapping after initial migration has no effect on existing permissions. PERMISSION_NAMES_MAPPING affects codename generation during migration and codename resolution at runtime. Changing the mapping updates both sides, but existing auth_permission rows retain their original codenames. If you need to change the mapping on an existing project, the permission rows and any group assignments must be updated to match.
list vs view codename collapse. If the mapping causes list and view to resolve to the same codename, the deduplication in get_builtin_permissions prevents duplicate permission rows. But the GET list and GET detail will then require the same codename, eliminating the ability to grant list access without detail-read access. Validate the list and detail behaviour separately.
Hardcoded permission strings bypass the mapping. Any project code that uses literal codename strings (such as user.has_perm("myapp.view_widget")) bypasses the mapping entirely. These strings must be manually audited and updated if the mapping changes.
Relevant Implementation Surface
- Python:
- core.default_settings.get_defaults
- core.patch_django
- ObjectPermissions
- ObjectPermissions.perms_map
- VUEDAPermissionsMixin.has_perm
- ListRowLevelViewSetMixin.apply_row_level_filter
- ModelInfoChoicesViewSet.get_queryset
- ModelInfoFilterSetChoicesViewSet.get_queryset
- WorkflowStateHistoryView.get
- WorkflowViewSet.object_state
- DynamicObjectPermissions.get_required_permissions
- REST: