---
url: "https://vueda.dev/v3/guides/permission-name-mapping.md"
title: "Map Django and VUEDA Permission Names"
---

# Map Django and VUEDA Permission Names

This guide sets the [Permission Mapping](/v3/reference/glossary.md#permission-mapping), places the import that applies it, and checks that the generated codenames match the ones that the server checks. [Permission Name Patch](/v3/core-concepts/configuration-surface-and-defaults.md#permission-name-patch) describes what the import changes in Django.

Settle the mapping before the project's first `migrate`. Migration writes one [Django: Permission](https://docs.djangoproject.com/en/6.1/ref/contrib/auth/#django.contrib.auth.models.Permission) row per codename, and a later mapping change does not rename those rows.

## Choose a Mapping

`PERMISSION_NAMES_MAPPING` renames each action in a model's [Django: Options.default\_permissions](https://docs.djangoproject.com/en/6.1/ref/models/options/#django.db.models.Options.default_permissions) when Django creates permission rows. VUEDA models declare `create`, `read`, `update`, `delete`, and `list` in [`default_permissions`](/v3/reference/api/py/vueda.core.models/BaseModelMeta.md#default_permissions). Every other model gets `list` added to its defaults, unless its `Meta` declares its own `default_permissions`.

VUEDA's viewsets honour two kinds of mapping:

- **The default**, from [core.default\_settings.get\_defaults](/v3/reference/api/py/vueda.core.default_settings.md#get_defaults). It maps `add` to `create`, `change` to `update`, and `view` to `read`. Use it for a new project.
- **Django's names.** It maps `create` to `add`, `update` to `change`, and both `read` and `list` to `view`. Use it for a database whose permission rows and group assignments already use Django's names.

[Permissions](/v3/reference/permissions.md#permission-name-mapping) lists the values for both.

The two differ in what the viewsets check. [ObjectPermissions](/v3/reference/api/py/vueda.core.permissions/ObjectPermissions.md) checks `create_`, `read_`, `list_`, `update_`, and `delete_` codenames. Importing [core.patch\_django](/v3/reference/api/py/vueda.core.patch_django.md) changes that for each mapping value that is `add`, `change`, or `view` and is not also a key. The import rewrites the matching entries of [`perms_map`](/v3/reference/api/py/vueda.core.permissions/ObjectPermissions.md#perms_map) to check that name.

::: warning
Use one of these two mappings exactly. Any other mapping, such as `"delete": "remove"`, creates permission rows that `ObjectPermissions` never checks. The [`check_permission_names_mapping`](/v3/reference/api/py/vueda.core.checks.md#check_permission_names_mapping) system check reports it as `vueda_core.E018`, which stops `migrate` and `runserver`.
:::

Under Django's names, every `GET` on a viewset requires the `view_` codename, for both list and detail. [Row-level list filtering](/v3/reference/api/py/vueda.core.permissions.md#filter_rows_for_user) and the choice endpoints check the same codename.

## Set Mapping and Patch Import Order

1. To use Django's names, set `PERMISSION_NAMES_MAPPING` in your base settings module, after the `get_defaults` call. For the default mapping, skip this step.

2. Import `patch_django` at the end of each leaf settings module, the module that `DJANGO_SETTINGS_MODULE` names. The VUEDA project template does this in `config/settings/local.py`:

    ```python
    # Import patch_django in the leaf settings file (not base.py) so that
    # PERMISSION_NAMES_MAPPING is fully resolved before the import triggers a lookup.
    # If you override PERMISSION_NAMES_MAPPING above, place this import after that override.
    from vueda.core import patch_django  # noqa: F401
    ```

    The template has only `local.py`. Add the same import to every other leaf module that you create, such as production or test settings. VUEDA does not import `patch_django` for you.

3. Run `python manage.py migrate`.

The import reads the mapping once to decide the `perms_map` rewrite, so a later change to the setting does not reach `ObjectPermissions`. This includes [Django: override\_settings](https://docs.djangoproject.com/en/6.1/topics/testing/tools/#django.test.override_settings) in tests. Codename generation, row-level list filtering, the choice endpoints, and [DynamicObjectPermissions](/v3/reference/api/py/vueda.core.permissions/DynamicObjectPermissions.md) read the mapping when they run.

## Check the Generated Codenames

List a model's codenames from `python manage.py shell`:

```python
from django.contrib.auth.models import Permission

Permission.objects.filter(content_type__app_label="myapp", content_type__model="widget").values_list(
    "codename", flat=True
)
```

For `myapp.Widget`, expect these rows:

| Mapping        | Codenames                                                                                                     |
| -------------- | ------------------------------------------------------------------------------------------------------------- |
| Default        | `myapp.create_widget`, `myapp.read_widget`, `myapp.update_widget`, `myapp.delete_widget`, `myapp.list_widget` |
| Django's names | `myapp.add_widget`, `myapp.view_widget`, `myapp.change_widget`, `myapp.delete_widget`                         |

Under Django's names, `read` and `list` both map to `view`, so the model has one `view_widget` row. Django's own models get the same set, for example `auth.list_group` under the default mapping.

## Check Runtime Permission Checks

Give a test user one permission at a time and send each request. The table uses the default names; substitute your mapped names.

| Request                                                                                                                 | Required permission                                                          | Result                                  |
| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------- |
| `GET` list                                                                                                              | `myapp.list_widget`                                                          | `200` if held, `403` if not             |
| `GET` detail                                                                                                            | `myapp.read_widget`                                                          | `200` if held, `403` if not             |
| `POST`                                                                                                                  | `myapp.create_widget`                                                        | `201` if held, `403` if not             |
| `PUT` or `PATCH`                                                                                                        | `myapp.update_widget`                                                        | `200` if held, `403` if not             |
| `DELETE`                                                                                                                | `myapp.delete_widget`                                                        | `204` if held, `403` if not             |
| [Field choices](/v3/reference/api/rest/vueda.info/vueda.info_model_info_choices_list.md)                     | `myapp.read_widget`, plus `list_` on the related model for a relation        | `200` if all held, `403` if any missing |
| [Filter choices](/v3/reference/api/rest/vueda.info/vueda.info_model_info_filter_choices_list.md)             | `myapp.read_widget`, plus `list_` on the related model for a queryset filter | `200` if all held, `403` if any missing |
| [Workflow state history](/v3/reference/api/rest/vueda.history/vueda.history_workflow_state_history_list.md) | `myapp.read_widget`                                                          | `200` if held, `403` if not             |
| [Workflow object state](/v3/reference/api/rest/vueda.workflow/vueda.workflow_workflows_object_state_retrieve.md) | `myapp.read_widget`                                                          | `200` if held, `403` if not             |

Under Django's names, the `GET` list and `GET` detail rows both require `myapp.view_widget`. VUEDA's own tests use the default mapping, so add these checks to your project's tests if you use Django's names.

## Troubleshooting

**Django and third-party models refuse every request.** Their rows use `add_`, `change_`, and `view_`, and they have no `list_` row. The leaf settings module in use does not import `patch_django`. VUEDA models are unaffected, because they declare CRUD action names. Check the settings module that each entry point uses: the server, the test runner, and `manage.py`.

**`manage.py check` reports `vueda_core.E018`.** The mapping is neither the default nor the full mapping to Django's names. Set it to one of the two in [Choose a Mapping](/v3/guides/permission-name-mapping.md#choose-a-mapping), then move group assignments from the old codenames to the new rows.

**List access cannot be granted without detail access.** A `view` mapping makes list and detail require the same codename.

**A mapping change has no effect on existing grants.** The next `migrate` adds rows under the new names. The old rows, the groups that hold them, and any [Group Permission Migration](/v3/reference/glossary.md#group-permission-migration) that names them keep the old codenames. Move those assignments to the new rows yourself.

**Project code checks the wrong codename.** A literal codename, such as `user.has_perm("myapp.read_widget")`, does not follow the mapping. Update these strings when you change the mapping.
