Model Access
Model access controls define CRUD (Create, Read, Update, Delete) permissions for models.
Access Control Lists
Section titled “Access Control Lists”Define which groups can perform operations on models.
Basic Access Definition
Section titled “Basic Access Definition”{ "data_type": "ModelAccess", "identifier": "access_crm_lead_user", "name": "CrmLead Access (User)", "model": "CrmLead", "group": "sales_user_group", "read_perm": true, "create_perm": true, "write_perm": true, "delete_perm": false}A
nameis required on everyModelAccessentry,modelis the model class name, andgroupis the group’s bareidentifier(nomodule.namedotted form).
Permission Types
Section titled “Permission Types”| Permission | Description |
|---|---|
read_perm | Can read/view records |
create_perm | Can create new records |
write_perm | Can update existing records |
delete_perm | Can delete records |
Access Patterns
Section titled “Access Patterns”Read-Only Access
Section titled “Read-Only Access”{ "data_type": "ModelAccess", "identifier": "access_product_portal", "name": "Product Access (Portal)", "model": "Product", "group": "core_portal", "read_perm": true, "create_perm": false, "write_perm": false, "delete_perm": false}Full Access for Managers
Section titled “Full Access for Managers”{ "data_type": "ModelAccess", "identifier": "access_crm_lead_manager", "name": "CrmLead Access (Manager)", "model": "CrmLead", "group": "sales_manager_group", "read_perm": true, "create_perm": true, "write_perm": true, "delete_perm": true}Public / Guest Access
Section titled “Public / Guest Access”Public (anonymous) visitors belong to the built-in core_public group. Grant guest
access by targeting that group — every ModelAccess entry names a group:
{ "data_type": "ModelAccess", "identifier": "access_product_public", "name": "Product Access (Public)", "model": "Product", "group": "core_public", "read_perm": true, "create_perm": false, "write_perm": false, "delete_perm": false}Multiple Access Rules
Section titled “Multiple Access Rules”Users get the union of all applicable permissions:
[ { "data_type": "ModelAccess", "identifier": "access_invoice_user", "name": "Invoice Access (User)", "model": "FinancialDocument", "group": "invoicing_user", "read_perm": true, "create_perm": true, "write_perm": false, "delete_perm": false }, { "data_type": "ModelAccess", "identifier": "access_invoice_manager", "name": "Invoice Access (Manager)", "model": "FinancialDocument", "group": "invoicing_admin", "read_perm": true, "create_perm": true, "write_perm": true, "delete_perm": true }]A user with the invoicing_admin group can do everything because:
- They inherit
invoicing_userpermissions viaimplied_groups - Plus their own manager permissions
Permission Inheritance via implied_groups
Section titled “Permission Inheritance via implied_groups”Permissions flow through the implied_groups chain. When defining a group hierarchy like:
[ { "data_type": "Group", "identifier": "invoicing_admin", "implied_groups": [["L", "invoicing_bookkeeper"]] }, { "data_type": "Group", "identifier": "invoicing_bookkeeper", "implied_groups": [["L", "invoicing_user"]] }, { "data_type": "Group", "identifier": "invoicing_user", "implied_groups": [["L", "core_internal"]] }]A user in invoicing_admin automatically gets all ModelAccess permissions from:
invoicing_admin(direct)invoicing_bookkeeper(implied)invoicing_user(implied by bookkeeper)core_internal(implied by user)
This means: You only need to define ModelAccess for the specific group level. Higher groups automatically inherit lower group permissions
Security File Organization
Section titled “Security File Organization”Security records (groups, model access, record rules) live in the module’s security/
directory. Every .yaml/.yml/.json file in that directory is auto-discovered and
imported — there is no data: manifest key to list them. Most modules keep all three
record kinds in a single security/security.yaml:
my_module/├── security/│ └── security.yaml # Groups + ModelAccess + RecordRule entries├── views/ # also auto-discovered├── data/ # also auto-discovered└── manifest.yamlThe manifest declares metadata and dependencies, not data files:
name: My Moduleidentifier: my_moduledependencies:- coreComplete Example
Section titled “Complete Example”[ { "data_type": "ModelAccess", "identifier": "access_sale_order_user", "name": "SaleOrder Access (User)", "model": "SaleOrder", "group": "sales_user_group", "read_perm": true, "create_perm": true, "write_perm": true, "delete_perm": false }, { "data_type": "ModelAccess", "identifier": "access_sale_order_manager", "name": "SaleOrder Access (Manager)", "model": "SaleOrder", "group": "sales_manager_group", "read_perm": true, "create_perm": true, "write_perm": true, "delete_perm": true }, { "data_type": "ModelAccess", "identifier": "access_sale_order_line_user", "name": "SaleOrderLine Access (User)", "model": "SaleOrderLine", "group": "sales_user_group", "read_perm": true, "create_perm": true, "write_perm": true, "delete_perm": true }, { "data_type": "ModelAccess", "identifier": "access_product_sales", "name": "Product Access (Sales User)", "model": "Product", "group": "sales_user_group", "read_perm": true, "create_perm": false, "write_perm": false, "delete_perm": false }]Checking Access in Code
Section titled “Checking Access in Code”Model access is enforced automatically by the ORM on every query and CRUD
operation — you don’t call a check function before reading or writing. An
unpermitted operation raises AccessError.
The current user’s merged permissions (the union across all their groups and
implied groups) are available on the environment via env.user_data["model_access"],
a list of per-model dicts ({model, read, write, create, delete}):
from fullfinity.engine.context import env_ctx
env = env_ctx.get()access = {a["model"]: a for a in env.user_data.get("model_access", [])}
if access.get("CrmLead", {}).get("read"): leads = await CrmLead.filter().all()Elevated Access (elevate())
Section titled “Elevated Access (elevate())”elevate() is the sudo / privilege-bypass primitive — the controlled escape
hatch when server code must act outside the current user’s permissions (a public
website route, a cross-tenant background job, a system computation):
from fullfinity.engine.base import elevate
# Regular query (respects access rules)leads = await CrmLead.filter().all()
# Elevated query (bypasses access checks)with elevate(): leads = await CrmLead.filter().all()What it bypasses — both layers at once:
- Model access (
ModelAccess) — the read/create/write/delete CRUD gate is skipped. - Record rules — no row-level rule WHERE clause is injected, and write/delete rule checks are skipped. (Controlled-edit field locks are also lifted.)
So inside an elevate() block the current user effectively sees and may modify
every record, exactly as the admin would.
Scope and caveats:
- Block-scoped, not record-scoped. Elevation is in effect for the entire
with elevate():body — every ORM operation reached from inside it, including nested calls into other model methods. It is restored on exit (even on exception). Keep the block as tight as possible around the privileged operation. - Propagates through
await. It is backed by aContextVar, so it applies to all awaited calls in the same task and nests correctly. - Does not bypass licensing. Enterprise licensing gates and enterprise-field
serialization are a separate mechanism —
elevate()does not unlock them. - Use it deliberately and narrowly; broad or long-lived elevation defeats the security model.
Best Practices
Section titled “Best Practices”- Start restrictive - Give minimal permissions, add more as needed
- Use group hierarchy - Managers inherit from users
- Document permissions - Comment why each access rule exists
- Test thoroughly - Verify users can/cannot do expected operations
Next Steps
Section titled “Next Steps”- Record Rules - Row-level security
- Groups and Users - Group management