---
url: "https://vueda.dev/v3/core-concepts/permission-model.md"
title: "Permission Model (CRUD + Object + State)"
---

# Permission Model (CRUD + Object + State)

VUEDA authorizes every API request on the server. A DRF permission class turns the request into a [CRUD](/v3/reference/glossary.md#crud) codename, and the user model decides that codename through four ordered [Permission Layers](/v3/reference/glossary.md#permission-layers). List and bulk delete requests also filter rows. This page describes the layer order, the codename mapping, where model scope and object scope apply, and why a refused request fails the way it does.

## Permission Authority Layers

[`VUEDAPermissionsMixin.has_perm`](/v3/reference/api/py/vueda.user.mixins/VUEDAPermissionsMixin.md#has_perm) decides one codename in four layers, in a fixed order. A check without an object runs only layer 1. A check with an object runs every layer that applies to it. Each layer that returns an answer replaces the decision so far, with one exception in layer 3.

**Layer 1: Baseline model permission.** The mixin calls [Django's `has_perm`](https://docs.djangoproject.com/en/6.1/topics/auth/customizing/#django.contrib.auth.models.PermissionsMixin.has_perm) without the object. Django's `ModelBackend` answers `False` to any check that passes an object, so VUEDA handles the object in the later layers. The result is the [Baseline Permission](/v3/reference/glossary.md#baseline-permission), and it becomes the starting decision.

**Layer 2: Workflow state overlay.** When the object belongs to a [Workflow-Enabled Model](/v3/reference/glossary.md#workflow-enabled-model), [`check_state_permission`](/v3/reference/api/py/vueda.workflow.models/WorkflowModelMethods.md#check_state_permission) evaluates the [State Permission](/v3/reference/glossary.md#state-permission) rules that match the object's current state, the user's groups, and the codename. A grant replaces a baseline denial, a deny replaces a baseline grant, and no matching rule keeps the baseline decision. [Workflow as a Permission Overlay](/v3/core-concepts/workflow-permission-overlay.md#state-permission-data-model) describes how matching rules combine.

**Layer 3: Row-level instance check.** When the model defines an inner `RowLevelPermissions` class, [`check_instance`](/v3/reference/api/py/vueda.core.permissions/BaseRowLevelPermissions.md#check_instance) evaluates the object against project rules. A `True` or `False` result replaces the decision from layers 1 and 2; `None` keeps it. Layer 3 is skipped when layer 2 returned a deny. `check_instance` does not see workflow state, so it cannot undo a state deny.

**Layer 4: Workflow-aware row-level check.** When the object belongs to a workflow-enabled model and the model defines `RowLevelPermissions`, [`check_instance_workflow`](/v3/reference/api/py/vueda.core.permissions/BaseRowLevelPermissions.md#check_instance_workflow) runs last. It runs regardless of the result of layer 2, and it receives that result as its `grant_or_deny` argument. Project logic here can therefore override a state deny. A `True` or `False` result is the final decision.

The same codename can therefore produce opposite decisions for two objects of one model. Two objects in different workflow states, or two objects that project row rules treat differently, can differ even though the user's baseline permission is the same for both.

## CRUD Codename and Action Mapping

VUEDA models declare `create`, `read`, `update`, `delete`, and `list` as their [`default_permissions`](/v3/reference/api/py/vueda.core.models/BaseModelMeta.md#default_permissions). The [`patch_django`](/v3/reference/api/py/vueda.core.patch_django.md) module renames Django's `add`, `change`, and `view` codenames through [Permission Mapping](/v3/reference/glossary.md#permission-mapping). It also adds `list` to every model that does not declare its own default permissions. The `auth_permission` rows therefore carry CRUD names from the first migration. [Map Django and VUEDA Permission Names](/v3/guides/permission-name-mapping.md) describes how to choose a mapping, why it must be settled before the first migration, and how to check the generated codenames.

With the default mapping, [`ObjectPermissions`](/v3/reference/api/py/vueda.core.permissions/ObjectPermissions.md) maps each HTTP method to a codename through its [`perms_map`](/v3/reference/api/py/vueda.core.permissions/ObjectPermissions.md#perms_map). `POST` requires `create`, `PUT` and `PATCH` require `update`, and `DELETE` requires `delete`. A `GET` for the `list` action requires `list`; every other `GET` action, such as retrieve or a detail extra action, requires `read`. List access and read access are separate decisions, so a user can hold either one without the other.

Codenames take the form `<app_label>.<action>_<model_name>`. For a model `myapp.Widget`, the five codenames are `myapp.create_widget`, `myapp.read_widget`, `myapp.update_widget`, `myapp.delete_widget`, and `myapp.list_widget`.

## Model Scope and Object Scope

DRF checks a request in two phases. The [Model-Scope Check](/v3/reference/glossary.md#model-scope-check) runs [`has_permission`](/v3/reference/api/py/vueda.core.permissions/ObjectPermissions.md#has_permission) before any object is fetched. It passes no object to `has_perm`, so only layer 1 applies. The [Object-Scope Check](/v3/reference/glossary.md#object-scope-check) runs [`has_object_permission`](/v3/reference/api/py/vueda.core.permissions/ObjectPermissions.md#has_object_permission) on the fetched object and passes it to `has_perm`, so all four layers apply. A `list` request has no object check; row filtering decides which rows it returns.

On a workflow-enabled model, `ObjectPermissions` can defer its own model-scope denial when a state rule grants the codename to one of the user's groups ([Model-Scope Deferral](/v3/reference/glossary.md#model-scope-deferral)). The request then reaches a later state-aware decision: the object check for object actions, or state filtering for `list`. Every other permission class on the viewset still has to pass, so authentication and application permission classes keep their authority. [Workflow as a Permission Overlay](/v3/core-concepts/workflow-permission-overlay.md) describes which actions defer.

## Row-Level Permissions

[Row-Level Permissions](/v3/reference/glossary.md#row-level-permissions) are opt-in project rules in a model's `RowLevelPermissions` class, built on [`BaseRowLevelPermissions`](/v3/reference/api/py/vueda.core.permissions/BaseRowLevelPermissions.md). [`check_queryset`](/v3/reference/api/py/vueda.core.permissions/BaseRowLevelPermissions.md#check_queryset) filters the rows of `list` and bulk delete requests in the database, and `check_instance` joins every object check as layer 3. Retrieve, update, and single-object delete do not filter rows; their object check decides. The two hooks are independent, so a project can list a row that an object check refuses, or the reverse. [Row-Level Permission Filtering](/v3/core-concepts/row-level-permission-filtering.md) describes both hooks, how filtering interacts with pagination and totals, and which rows bulk delete accepts.

## Workflow Overlay and Transition Gates

[State Permission](/v3/reference/glossary.md#state-permission) rules grant or deny the same CRUD codenames that baseline permissions use, so the [Workflow Overlay](/v3/reference/glossary.md#workflow-overlay) adds no separate permission namespace. They form layer 2 of an object check, and [`filter_rows_for_user`](/v3/reference/api/py/vueda.core.permissions.md#filter_rows_for_user) applies them to `list` rows even when the model defines no `RowLevelPermissions`. Executing a transition is a separate check: the user needs every [Workflow Permission](/v3/reference/glossary.md#workflow-permission) and [Transition Permission](/v3/reference/glossary.md#transition-permission) that the permission rows name, and the transition must leave the object's current state. A workflow or transition with no permission rows denies every user. [Workflow as a Permission Overlay](/v3/core-concepts/workflow-permission-overlay.md) describes state rule evaluation, the transition gates, and the gate that each workflow endpoint applies.

## Model Info Access

[Model Info](/v3/reference/glossary.md#model-info) has its own permission gate. The gate is separate from the permissions of the model that the metadata describes. Each model info entry is a [Content Type](/v3/reference/glossary.md#content-type), so `ObjectPermissions` checks content type codenames on [`ModelInfoViewSet`](/v3/reference/api/py/vueda.info.viewsets/ModelInfoViewSet.md).

Getting model info for one model requires `contenttypes.read_contenttype`. The list of registered models requires `contenttypes.list_contenttype`. A user without them gets `403` from model info, even when the user holds every permission on the model itself. The client loads model info before it opens a model's route, so that user cannot open any model in the client. Grant both permissions to every group whose users work in the client.

These two permissions grant access to metadata only. Each request to the model's own endpoints still needs the model's own codename.

## Server Authority and UI Visibility

Every decision on this page is server enforcement. The client reads permission results to choose which routes, buttons, and actions to show, and that choice is UI behavior only. An object's [Available Actions](/v3/reference/glossary.md#available-actions) are the actions that the server allows on it; [Action Contract and Availability](/v3/core-concepts/action-contract-and-availability.md) describes how the server computes them. [Authorization vs UI Semantics](/v3/core-concepts/authorization-vs-ui-semantics.md) describes where server enforcement ends and UI visibility begins.

## Why Refused Requests Fail Differently

The layer that refuses a request decides how the refusal looks. The [Permissions reference](/v3/reference/permissions.md) lists the status code for each case.

**A model-scope denial stops the request before VUEDA fetches any object.** The response reveals nothing about any row.

**An object-scope denial hides an object the user cannot read.** VUEDA keeps the behavior of DRF's [`DjangoObjectPermissions`](https://www.django-rest-framework.org/api-guide/permissions/#djangoobjectpermissions). A refused read answers as if the object did not exist. A refused write answers the same way when the user also lacks `read` on the object, and as a denial when the user can read it. On retrieve, the refusal comes from layers 2 to 4: a state deny, `check_instance`, or `check_instance_workflow`.

**A list never fails because of row rules.** Filtering removes rows before pagination, so a user with `list` permission receives the rows they may see, possibly none.

**Bulk delete refuses the whole batch.** When any requested pk is missing, filtered out, or refused by its object check, [`destroy`](/v3/reference/api/py/vueda.core.viewsets/VuedaViewSet.md#destroy) deletes nothing. It reports each such pk with the message `"Object with pk=... does not exist."`. Missing and refused rows get the same message, so the response does not reveal which rows exist.

**Workflow endpoints check `vueda_workflow.read_workflow` first.** [`WorkflowViewSet.check_permissions`](/v3/reference/api/py/vueda.workflow.viewsets/WorkflowViewSet.md#check_permissions) applies this gate before any object check, so it refuses a user who lacks that permission even when the user's object permissions would allow the request. Current object state and workflow state history do not require it; they follow the object's own `read` permission. For [`permitted_transitions`](/v3/reference/api/rest/vueda.workflow/vueda.workflow_workflows_permitted_transitions_list.md), a model that does not enable `class Vueda.Workflow` answers an empty list to any user who can read the model. An enabled model with no workflow definition raises `WorkflowNotConfiguredError`.

**Transition refusals are validation errors.** After the object's own `read` check passes, a missing workflow or transition permission is reported as a validation error. So is an unknown transition code, or a transition that does not leave the current state. A row that another request holds locked is reported the same way, with the message `"This object cannot be updated right now. Please try again."`.

**A state grant can move a denial from model scope to object scope.** Only a grant that matches the user's groups, the action's codename, the model's content type, and its workflow defers a model-scope denial. Unrelated rules and state denies leave that denial in place. After deferral, the object check decides each object through all four layers, and a list returns only the rows that a state rule grants.
