Entity Security
📄 Overview
The Entity Security system provides a comprehensive, multi-layered approach to controlling access to data and operations within the platform. All security rules are enforced at the API (OData) level, ensuring consistent security across all access points.
The security architecture consists of three complementary layers, each addressing different aspects of access control:
- Role-Based Access Control — Basic permissions based on user roles
- Data Filtering — Determines which objects a user can access
- Contextual Predicates — Fine-grained rules based on object state and user context
This multi-layered approach allows for both simple role-based permissions and complex, context-sensitive access rules that adapt to your business requirements.
🔄 Authorization Process
When a user attempts to access or modify data through the API, the following process occurs:
-
Role-Based Check (Layer 1):
The system verifies if the user's roles grant the basic permission required for the operation (e.g., Read, Write, or Create).
If this check fails, access is denied immediately. If it passes, the process continues to the next layer.
-
Data Filtering (Layer 2):
Filter Functions limit which objects the user can access based on their roles and context.
For read operations, filters determine which objects appear in results. For write operations, they determine if the target object is accessible.
If an object isn't accessible through filters, operations like PUT, POST, or DELETE will return NotFound.
-
Contextual Predicates (Layer 3):
For objects that pass the previous layers, Predicate Functions evaluate fine-grained permissions based on the specific object's state, the user's context, and business rules.
Predicates can restrict operations at the field level or for the entire entity.
Predicates are evaluated in sequence, and processing stops at the first denial.
This layered approach ensures that security checks proceed from simpler to more complex, optimizing performance while maintaining comprehensive protection.
🛡️ Layer 1: Role-Based Access Control
The first security layer establishes basic permissions based on user roles. These permissions serve as gates that must be passed before more granular security rules are evaluated.
Resource Type | Permission | Description |
---|---|---|
Entity | Read | Ability to retrieve entities of this type |
Write | Ability to update existing entities of this type | |
Create | Ability to create new entities of this type | |
Delete | Ability to delete entities of this type | |
Field | Inherit | Field inherits permissions from its parent entity (default) |
Read | Ability to view the field value | |
Write | Ability to modify the field value | |
Calculation Function | Inherit | Function inherits the Entity's Write permission (default) |
Invoke | Ability to call the function through the API | |
Entity Filter | Inherit | Filter inherits the Entity's Read permission (default) |
Invoke | Ability to use the filter in queries | |
Entity Predicate | Inherit | Predicate inherits the Entity's Read permission (default) |
Read | Predicate results are included in the $Predicates collection of API responses |
These role-based permissions are configured through the administration interface, where roles can be assigned to users and permissions can be assigned to roles.
🔍 Layer 2: Data Filtering
The second security layer determines which objects a user can access. Filters are applied based on the user's roles to limit the visible subset of data.
Important:
Filters are applied to all operations, not just read operations. If a user attempts to update or delete an object that their filters exclude, the operation will fail with a NotFound response.
How Filters Are Applied
For each entity type, you can configure role-filter pairs that determine which Filter Functions are applied based on the user's roles.
When a user accesses an entity:
- The system identifies which roles the user belongs to
- For each role that has a configured filter, the corresponding Filter Function is applied
- Multiple filters are combined with AND logic — objects must satisfy all applicable filters
- If no filters are configured for any of the user's roles, no filtering is applied
Example: Regional Access Control
-- Filter Function limiting users to their assigned region
CREATE FUNCTION filter_by_user_region(userId VARCHAR(255), args TEXT)
RETURNS SETOF "Order" AS $
DECLARE
userRegionId VARCHAR(255);
BEGIN
-- Get the user's assigned region
SELECT "RegionId" INTO userRegionId
FROM "UserProfile"
WHERE "UserId" = userId;
-- Return only orders from the user's region
RETURN QUERY
SELECT o.*
FROM "Order" AS o
WHERE o."RegionId" = userRegionId;
END;
$ LANGUAGE plpgsql STABLE;
In this example, when a Regional Manager accesses orders, they will only see orders from their assigned region, regardless of how they query the data.
🧩 Layer 3: Contextual Predicates
The third and most granular security layer uses predicate functions to evaluate access permissions based on the specific object's state, the user's context, and complex business rules.
While the previous layers operate on collections of objects, predicates focus on individual object instances and provide fine-grained control over what operations are permitted.
How Predicates Are Applied
For each entity type, you can configure role-predicate pairs that determine which Predicate Functions are evaluated based on the user's roles.
Note:
For predicates, evaluation order matters. Predicates are evaluated in sequence, and processing stops at the first denial. This can impact both performance and security behavior.
Predicates can control:
- Entity-level Write/Delete access — Whether the entity can be modified or deleted
- Field-level Read/Write access — Whether specific fields can be viewed or modified
- Function invocation — Whether specific functions can be called on the entity
- UI behavior — Whether predicate results are exposed to the client for UI customization
Example: Status-Based Edit Restrictions
-- Predicate allowing edits only for non-finalized invoices
CREATE FUNCTION is_invoice_editable(
objectId VARCHAR(255),
userId VARCHAR(255),
args TEXT DEFAULT NULL
)
RETURNS BOOLEAN AS $$
DECLARE
v_status TEXT;
BEGIN
-- Get the invoice status
SELECT "Status" INTO v_status
FROM "Invoice"
WHERE "Id" = objectId;
-- Return TRUE if invoice is in an editable state
RETURN v_status IN ('Draft', 'Pending');
END;
$$ LANGUAGE plpgsql STABLE;
When this predicate is applied to the Invoice entity for a role, users with that role will only be able to edit invoices that are in 'Draft' or 'Pending' status, regardless of their role-based permissions.
Client-Side Integration
The system provides two mechanisms for client-side integration with the security system:
1. Predicate Results
Predicate results are automatically exposed to the client through the OData API. Each object returned includes a system collection named $Predicates
, containing the results of all readable predicates for that object:
// Example API response with predicate results
{
"Id": "INV-2023-00123",
"CustomerName": "Acme Corp",
"Status": "Approved",
// ... other fields
// System collection of predicate results
"$Predicates": {
"is_invoice_editable": false,
"can_void_invoice": true,
"requires_approval": false
}
}
2. Permissions Information
When using the expandPermissions
parameter in GET requests, the API also returns a $Permissions
collection that provides calculated permission information:
// Example API response with expanded permissions
{
"Id": "INV-2023-00123",
"CustomerName": "Acme Corp",
"Status": "Approved",
// ... other fields
// System collection of predicate results
"$Predicates": {
"is_invoice_editable": false,
"can_void_invoice": true
},
// System collection of calculated permissions
"$Permissions": {
"Entity": {
"Write": false,
"Delete": true
},
"Fields": {
"Status": { "Write": false },
"CustomerName": { "Write": true }
},
"CalculationFunctions": {
"recalculate_invoice": { "Invoke": true },
"void_invoice": { "Invoke": true }
}
}
}
This allows client applications to adjust their UI based on the same security rules that govern the API, creating a consistent user experience.
Security for Expanded Collections
When using OData's $expand
parameter to include related entities in a response, all security rules are consistently applied to these expanded collections:
Important:
Security rules are applied consistently across all data access patterns, including expanded collections. There are no "back doors" through navigation properties.
Security is enforced for expanded collections in the following ways:
- Role-Based Check (Layer 1): The system verifies if the user has Read permission for the expanded entity type. If not, the expanded property will be empty.
- Data Filtering (Layer 2): All applicable filters for the user's roles are applied to the expanded collection, limiting the returned items to those the user is allowed to see.
- Field-Level Security: Field-level permissions are applied to each object in the expanded collection, controlling which fields are visible.
Example
GET /Customers(1)?$expand=Orders
// If the user has access to Orders but their filters limit visibility to certain regions:
{
"Id": "CUST-001",
"Name": "Acme Corporation",
// ... other customer fields
// Only orders that pass security filters are included
"Orders": [
{ "Id": "ORD-123", "Total": 1500.00, ... }, // This order is in the user's region
{ "Id": "ORD-456", "Total": 2300.00, ... } // This order is also in the user's region
// Orders from other regions are not included, even though they exist
]
}
If no items in the expanded collection pass the security filters, an empty array is returned rather than null
:
{
"Id": "CUST-001",
"Name": "Acme Corporation",
// ... other customer fields
// Empty array because no orders pass the security filters for this user
"Orders": []
}
This ensures that security is enforced consistently across your application, regardless of how the data is accessed.
📋 Practical Example
To illustrate how all three security layers work together, consider the following example for an Invoice management system:
Security Layer | Sales Rep | Regional Manager | Finance Admin |
---|---|---|---|
Layer 1: Role-Based Permissions |
|
|
|
Layer 2: Data Filtering |
filter_by_creator (only sees own invoices) |
filter_by_region (sees all invoices in region) |
No filters (sees all invoices) |
Layer 3: Contextual Predicates |
is_invoice_editable (only Draft status) |
is_invoice_editable (Draft or Pending status) can_approve_invoice (only if amount < $10,000) |
is_invoice_editable (any status except Paid) can_approve_invoice (any amount) |
Scenario: Updating an Invoice
When a Sales Rep attempts to update an invoice with ID "INV-2023-00456" and status "Pending":
-
Layer 1: The system checks if Sales Reps have Write permission for Invoices
→ Access denied (Sales Reps don't have Write permission)
When a Regional Manager attempts the same update:
-
Layer 1: The system checks if Regional Managers have Write permission for Invoices
→ Access granted -
Layer 2: The system applies the
filter_by_region
filter to check if the invoice belongs to the manager's region
→ Access granted (invoice is in the manager's region) -
Layer 3: The system evaluates the
is_invoice_editable
predicate to check if the invoice's status allows editing
→ Access granted (status "Pending" is editable for managers)
This multi-layered approach ensures that security rules are applied consistently and comprehensively across all operations.
💡 Best Practices
-
Start simple, add complexity as needed:
Begin with role-based permissions (Layer 1) and add filters and predicates only when needed for specific business rules.
-
Optimize predicate evaluation order:
Place the most restrictive and fastest predicates first to improve performance through early rejection.
-
Use descriptive names:
Name predicates and filters according to what they check, such as
is_owner
orfilter_by_department
. -
Document your security model:
Maintain documentation for your security configuration to ensure consistency and ease of maintenance.
-
Test security rules thoroughly:
Use the testing tools to verify that your security rules behave as expected across different user roles and scenarios.
📖 Further Reading
- Entity Predicate Functions — Learn more about creating and using predicate functions
- Entity Filter Functions — Learn more about creating and using filter functions
- Role Management — Learn how to configure user roles and permissions