Skip to main content

Policy Rule Filter Expressions

Devii's Default Policy: "Deny by Default"

In Devii, the security system is designed with the principle of "deny by default." This means no database access is granted to any role unless explicitly allowed by a policy rule. Please read the Security Engine documentation for more in-depth information.

When a new role is added to your tenant, it will not automatically have access to your database. To grant access, you must create a policy rule that specifies what the role can access and under what conditions.

  • To grant access to all users, use a global rule:
    • Using the Portal (recommended): Toggle "Global" under "Policy Rules" to enable it (disables role/role class restrictions). Define the rule, such as granting 'select' permissions for all tables or only tables that everyone should view, with no additional operations.
    • With GraphQL, use the roles_pbac endpoint and the create_policy_rule mutation, leaving "classes" and "roles" empty.

Writing Filters for Policy Rules: Navigating Nested Relationships in GraphQL

Policy rules grant access to database tables, while filter expressions narrow that access by defining specific conditions. For GraphQL schemas with nested relationships, filters ensure rules align with the relationships between fields, allowing precise and secure access control tailored to your target table's structure. This guide provides a generalized approach to writing policy filters, helping you navigate complex schemas while ensuring they align with your target table's structure.

Understanding Nested Relationships in GraphQL

In many GraphQL schemas, top-level types contain nested fields representing related objects. Here's a generic example:

Generic Top-Level Schema

# Schema Example

type parents {
parentid: ID! # Unique identifier for the parent object
name: String # Name or description of the parent
child_collection: [child] # A collection of related child objects
}

type child {
childid: ID! # Unique identifier for the child object
roleid: ID! # Role associated with the child
parentid: ID! # The parent this child belongs to
parent: parents # Reference to the parent object for additional details
}
  • parents: The top-level type, representing entities like companies, projects, or organizations.
  • child_collection: A collection of related child objects linked to the parent.
  • Child: Represents a related entity, such as an employee or team member and includes:
    • roleId: Role associated with the user.
    • parentId: Foreign key linking the child to its parent.
    • parent: Reference to the parents object for additional details.

Generic Target Table:

Represents entities tied to the parents and indirectly to their nested child entities.

# Target Table Schema

type target {
itemid: ID! # Unique identifier for the target target object.
name: String! # name of item in target table
parentid: Bigint! # Foreign key linking the target to the parents table
child_collection: [child] # A collection of related child objects
parent: parents # Reference to the parent object for additional details
}
  • itemid: Unique identifier for the target object.
  • name: A string representing the item in the target table.
  • parentid: Foreign key linking the target to the parents table.
  • child_collection: A collection of related child objects linked to the parent.
  • child: Represents a related entity, such as an employee or team member and includes:
    • roleid: Role associated with the user.
    • parentid: Foreign key linking the child to its parent.
    • parent: Reference to the parents object for additional details.

Relationships in GraphQL

The target table connects to parents through the parentid foreign key. This creates both direct and indirect relationships:

  • Direct Relationship:
    • target.parentidparents.parentid: Direct link to the parents entity.
  • Indirect Relationship:
    • target.parentidchild.parentid: Since child entities are nested within parents, the target table can reference related child entities via the shared parentid.

Use Case Summary

  • parents: Acts as the root entity in the hierarchy.
  • child: Provides details about sub-entities within the parents.
  • target: Links to parents and can indirectly access nested child details, enabling powerful querying capabilities for hierarchical data structures.

Steps for Writing Policy Filters

When defining policy filters, assume the target table (e.g., locations, departments) includes a parentid column. Refer to the GraphQL schema above for context.

  1. Understand the Relationships:
    1. Identify the top-level type (parents) and the nested collection (child_collection).
    2. Determine which fields establish relationships (e.g., roleid, parentid) between the target table and related entities.
  2. Define the Filter Logic:
  • Use relationships (e.g., parentid) and role attributes (e.g., $_PRINCIPAL.roleid) to define access rules. Refer to Filtering Special Note for additional details on $_PRINICIPAL.xxxxxxx.
  1. Write the Filter:
  • Use the ANY operator to check conditions dynamically in the nested collection.
  • Ensure the filter matches the schema's field names and relationships.
  1. Verify Target Table Compatibility:
  • Verify the child_collection correctly references its parent entity (e.g., parent field in child).
  • Ensure the target table includes a column (e.g., parentid) required by the policy filter for enforcing relationships.

Example Use Case: Enforcing Access to Nested Data

Here’s an example filter applied to the target table:

parent.child_collection ANY(roleid = $_PRINCIPAL.roleid).parentid

Explanation:

  1. parent.child_collection: Accesses the nested collection of child entities within parents.

  2. ANY: Dynamically checks all entries in the nested collection.

  3. roleid = $_PRINCIPAL.roleid: Ensures only entities matching the user's role are considered.

  4. .parentid: equals target.parentid, matches the parentid in the nested structure to the parentid in the target table.

This will enable anyone with the correct role id to view or modify the target table, depending on the capabilities given in the policy rule

Key Considerations

Ensure Column Compatibility: The target table must include a column (e.g., parentid) to match against the policy filter.

Dynamic Attribute Matching: Use variables like $_PRINCIPAL.roleid to reference user-specific attributes dynamically.

Relationship Verification: Confirm the child_collection references the parent (e.g., Child.parent points to parents).

Testing and Validation: Test the filter with various scenarios to confirm its correctness.

Avoiding Common Mistakes

Incorrect Relationships: Ensure the child_collection references the parent properly (e.g.,child.parentid aligns with parents.parentid).

Mismatched Columns: Verify that the filter references valid fields in the schema.

Ambiguous Field Names: Use clear and consistent field names to avoid confusion (e.g., parentid vs. companyid).

Summary

Filters like parent.child_collection ANY(roleid = $_PRINCIPAL.roleid).parentid provide granular access control by leveraging relationships within GraphQL schemas. By understanding your schema and defining clear relationships, you can enforce policies effectively while maintaining flexibility for future changes.

Examples:

Policy Rules are easiest to create using the Devii Portal, you can find instructions HERE

Only Managers Can CRUD Company Locations Where They Are Employed.

Schema


companies{
companyid: ID!
name: String!
address: String!
employees_collections: employees
}

employees{
roleid: ID!
name: String!
locationid: Bigint!
company: companies
location: locations
}

locations{
locationid: ID!
companyid: Bigint!
name: String!
employee_collection: employees
}

Steps to Create Filter

  1. Define what the rule should do:
    • I would like a rule where only managers can create, update and delete company locations where they are employed.
  2. Create role class “managers”, so I can apply that group to the rule
    • This role class can be reused for other rules
    • Instructions for creating a Role Class in portal can be found HERE
  3. Add users to that role class
  4. Create rule “managers can select, delete, update or insert a location within their company”
  5. Select “Insert”, "Select", “Update” and “Delete” for capabilities
    • Corresponds to Create, Read, Update and Delete in CRUD operations
  6. Select targeted tables (in this case it will be the locations table that has a column companyid that will relate to the filter I will create)
    • Select locations table as this is the table that "managers" will need to update
  7. Find table in your schema that will relate back to the locations table that also will include roleids and companyid as they are essential to the policy filter
    • In this case we will use the child table employee_collection inside companies as it will contain both the companyid via company
      • The roleid will be contained within the role class "managers" that is assigned to this rule.
  8. Create policy filter
    • company.employees_collection ANY(roleid = $_PRINCIPAL.roleid).companyid
      • This policy filter will ensure that users roleid has the companyid must match the column companyid in locations table
GraphQL of Policy
query Locations ($f: String) {
locations (filter: $f){
companyid
company {
employees_collection {
roleid

}
}
}
}

variables:

{
"f": "company.employees_collection ANY(roleid = $_PRINCIPAL.roleid).companyid"

}

The roleid represents a manager of a company, and the ANY filter matches all managers with the same roleids. This rule is structured to start at the child level (employees_collection) and ascend to the target table (locations).

Explanation of the Filter

  • employee_collection traverses the employees table.
  • ANY(roleid = $_PRINCIPAL.roleid) ensures the principal's roleid matches an employee's roleid.
  • .companyid ensures the companyid in the locations table matches the employee's company.

Example Use Case

Scenario: An employee with roleid = 1234 wants to update the name of a location they manage, identified by locationid = 1. The filter would now be (company.employees_collection ANY(roleid = 1234).companyid) it will find all companyid values in the locations table where at least one employee in the employees_collection has the roleid = 1234. For clarity I am adding in the locationid.

Updated Query:

query Locations($f: String) {
locations(filter: $f) {
locationid
name
companyid
employee_collection {
roleid
name
}
}
}

variables:

{
"f": "company.employee_collection ANY(roleid = 1234).companyid"
}

Expected Response:

{
"data": {
"locations": [
{
"locationid": 1,
"name": "Main Office",
"companyid": 100,
"employee_collection": [
{ "roleid": "1111", "name": "Alice" },
{ "roleid": "9874", "name": "Bob" },
{ "roleid": "1234", "name": "Charlie" }
]
}
]
}
}
Final Explanation
  • The query identifies all locations managed by employees with roleid = 1234.
  • The ANY filter ensures that the roleid matches an employee in employee_collection whose companyid aligns with the locations table.
  • Managers with roleid = 1234 can update the name of the location, as can employees with roleid = 9874 and roleid = 1111.

If a user with roleid = 2345 attempts this action, the filter will not return any matching results, preventing them from altering the location.