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:

  1. Role-Based Access Control — Basic permissions based on user roles
  2. Data Filtering — Determines which objects a user can access
  3. 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:

Entity Security Authorization Flow Layer 1: Role-Based Access Control API Request Has Role Permission? Layer 2: Data Filtering Object passes filters? Layer 3: Contextual Predicates Passes all predicates? Access Denied Access Granted No Yes No Yes No Yes Checks if user's roles have the required permissions: • Read, Write, Delete • Field-level permissions Applies role-based filters to limit visible data: • Multiple filters use AND logic • Applied to all operations Evaluates contextual rules: • Based on object state • Sequential evaluation • First denial stops processing
  1. 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.

  2. 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.

  3. 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:

  1. The system identifies which roles the user belongs to
  2. For each role that has a configured filter, the corresponding Filter Function is applied
  3. Multiple filters are combined with AND logic — objects must satisfy all applicable filters
  4. 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:

  1. 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.
  2. 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.
  3. 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
  • Invoice: Read, Create
  • Total Field: Read only
  • Invoice: Read, Create, Write
  • All fields: Read, Write
  • Invoice: Read, Create, Write
  • All fields: Read, Write
  • Can invoke all functions
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":

  1. 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:

  1. Layer 1: The system checks if Regional Managers have Write permission for Invoices
    → Access granted
  2. 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)
  3. 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 or filter_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