Record Rules
Record rules provide row-level security, controlling which specific records a user can access.
Overview
Section titled “Overview”While model access controls CRUD operations on entire models, record rules filter which records within a model are visible.
Basic Record Rule
Section titled “Basic Record Rule”{ "data_type": "RecordRule", "identifier": "rule_lead_salesperson", "name": "Leads: salesperson sees own", "model": "CrmLead", "groups": ["crm_user"], "rule": "(Q(salesperson__id__eq=uid))", "read_perm": true, "write_perm": true, "create_perm": true, "delete_perm": false}This rule ensures CRM users can only see leads assigned to them.
Rule Syntax
Section titled “Rule Syntax”Rules use Q expressions to filter records.
Eval Namespace
Section titled “Eval Namespace”A rule string is parsed by the same safe Q-evaluator used for view modifiers
(ast-walked against a whitelist — never raw eval). Inside a rule you may
reference Q, the connectors & | ~, Python literals, and the following
context names bound from the current request’s environment:
| Name | Binds to |
|---|---|
Q | The Q-expression builder (the only callable, alongside the D date helper). |
uid | The current user’s id. |
cid | The primary (first) active company id, or None. |
company_id | An exact alias of cid — the same primary company id. |
cids | A list of all active company ids (the user’s allowed companies). Use with __in for cross-company rules, e.g. Q(company__id__in=cids). |
contact_id | The current portal contact id. |
"(Q(company__id__in=cids))" # records in ANY of the user's active companies"(Q(company__id__eq=cid))" # records in just the primary companyCurrent User
Section titled “Current User”"(Q(user__id__eq=uid))" # Records owned by current user (ManyToOne)"(Q(salesperson__id__eq=uid))" # Records assigned to current user (ManyToOne)"(Q(create_uid__id__eq=uid))" # Records created by current user (ManyToOne)Important: For ManyToOne fields, use field__id__eq=uid syntax to traverse the relationship and compare the ID.
Field Comparisons
Section titled “Field Comparisons”"(Q(active=True))" # Only active records"(Q(state='draft'))" # Only draft records"(Q(amount__gt=0))" # Positive amountsCombining Conditions
Section titled “Combining Conditions”# AND conditions"(Q(active=True) & Q(user__id__eq=uid))"
# OR conditions"(Q(user__id__eq=uid) | Q(team__member__id__eq=uid))"Related Fields
Section titled “Related Fields”# Through relationships"(Q(company__id__eq=cid))" # Current user's company"(Q(team__manager__id__eq=uid))" # User is team managerRule Modes
Section titled “Rule Modes”Restrictive Rules (Default)
Section titled “Restrictive Rules (Default)”Multiple rules for the same model/groups are combined with OR:
[ { "data_type": "RecordRule", "identifier": "rule_lead_own", "model": "CrmLead", "groups": ["crm_user"], "rule": "(Q(salesperson__id__eq=uid))" }, { "data_type": "RecordRule", "identifier": "rule_lead_team", "model": "CrmLead", "groups": ["crm_user"], "rule": "(Q(team__member__id__eq=uid))" }]Users see leads where they’re the salesperson OR they’re a team member.
Global Rules
Section titled “Global Rules”Rules without groups apply to everyone:
{ "data_type": "RecordRule", "identifier": "rule_lead_active_only", "name": "Leads: active only", "model": "CrmLead", "groups": [], "rule": "(Q(active=True))"}Global rules are combined with AND with other rules.
Permission Scope
Section titled “Permission Scope”Control which operations the rule affects:
{ "data_type": "RecordRule", "identifier": "rule_invoice_company", "model": "Invoice", "groups": ["account_user"], "rule": "(Q(company__eq=company_id))", "read_perm": true, "write_perm": true, "create_perm": true, "delete_perm": false}| Permission | Description |
|---|---|
read_perm | Rule applies to read operations |
write_perm | Rule applies to update operations |
create_perm | Rule applies to create operations |
delete_perm | Rule applies to delete operations |
Common Patterns
Section titled “Common Patterns”Own Records Only
Section titled “Own Records Only”{ "data_type": "RecordRule", "identifier": "rule_task_own", "model": "Task", "groups": ["project_user"], "rule": "(Q(assignee__id__eq=uid))"}Company-Based Multi-Tenancy
Section titled “Company-Based Multi-Tenancy”{ "data_type": "RecordRule", "identifier": "rule_contact_company", "model": "Contact", "groups": ["core_internal"], "rule": "(Q(company__eq=company_id) | Q(company__isnull=True))"}Team-Based Access
Section titled “Team-Based Access”{ "data_type": "RecordRule", "identifier": "rule_lead_team", "model": "CrmLead", "groups": ["crm_user"], "rule": "(Q(team__member__id__eq=uid))"}Manager Sees All Team Records
Section titled “Manager Sees All Team Records”{ "data_type": "RecordRule", "identifier": "rule_lead_manager", "model": "CrmLead", "groups": ["crm_manager"], "rule": "(Q(id__gte=0))"}Portal User Access
Section titled “Portal User Access”{ "data_type": "RecordRule", "identifier": "rule_order_portal", "model": "SaleOrder", "groups": ["core_portal"], "rule": "(Q(contact__eq=contact_id))"}Complete Security Setup
Section titled “Complete Security Setup”[ { "data_type": "Group", "identifier": "sales_user", "name": "User", "category": "Sales" }, { "data_type": "Group", "identifier": "sales_manager", "name": "Manager", "category": "Sales", "implied_groups": [["L", "sales_user"]] }, { "data_type": "ModelAccess", "identifier": "access_order_user", "name": "SaleOrder Access (User)", "model": "SaleOrder", "group": "sales_user", "read_perm": true, "create_perm": true, "write_perm": true, "delete_perm": false }, { "data_type": "ModelAccess", "identifier": "access_order_manager", "name": "SaleOrder Access (Manager)", "model": "SaleOrder", "group": "sales_manager", "read_perm": true, "create_perm": true, "write_perm": true, "delete_perm": true }, { "data_type": "RecordRule", "identifier": "rule_order_user", "name": "Orders: salesperson own", "model": "SaleOrder", "groups": ["sales_user"], "rule": "(Q(salesperson__id__eq=uid))" }, { "data_type": "RecordRule", "identifier": "rule_order_manager", "name": "Orders: manager all", "model": "SaleOrder", "groups": ["sales_manager"], "rule": "(Q(id__gte=0))" }]Bypassing Record Rules
Section titled “Bypassing Record Rules”Use elevate() context manager when needed:
from fullfinity.engine.models import elevate
# Normal query (respects record rules)my_orders = await SaleOrder.filter().all()
# Elevated query (bypasses record rules)with elevate(): all_orders = await SaleOrder.filter().all()Debugging Rules
Section titled “Debugging Rules”Enable debug logging to see applied rules:
import logginglogging.getLogger("fullfinity.security").setLevel(logging.DEBUG)Best Practices
Section titled “Best Practices”- Start with global rules - Apply company/active filters globally
- Layer group rules - More privileged groups get broader access
- Test as different users - Verify rules work as expected
- Document complex rules - Explain the business logic
- Use meaningful names - Rule names should describe what they do
Next Steps
Section titled “Next Steps”- Groups and Users - Managing groups
- Model Access - CRUD permissions