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_pbacendpoint and thecreate_policy_rulemutation, 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 theparentsobject 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 thetargetto theparentstable.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 theparentsobject 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.parentid↔parents.parentid: Direct link to theparentsentity.
- Indirect Relationship:
target.parentid↔child.parentid: Sincechildentities are nested within parents, thetargettable can reference relatedchildentities via the sharedparentid.
Use Case Summary
parents: Acts as the root entity in the hierarchy.child: Provides details about sub-entities within theparents.target: Links toparentsand can indirectly access nestedchilddetails, 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.
- Understand the Relationships:
- Identify the top-level type (
parents) and the nested collection (child_collection). - Determine which fields establish relationships (e.g.,
roleid,parentid) between the target table and related entities.
- Identify the top-level type (
- 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.
- Write the Filter:
- Use the
ANYoperator to check conditions dynamically in the nested collection. - Ensure the filter matches the schema's field names and relationships.
- Verify Target Table Compatibility:
- Verify the
child_collectioncorrectly references its parent entity (e.g.,parentfield inchild). - 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:
-
parent.child_collection: Accesses the nested collection of child entities within parents. -
ANY: Dynamically checks all entries in the nested collection. -
roleid = $_PRINCIPAL.roleid: Ensures only entities matching the user's role are considered. -
.parentid: equalstarget.parentid, matches theparentidin the nested structure to theparentidin 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
- 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.
- 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
- Add users to that role class
- Create rule “managers can select, delete, update or insert a location within their company”
- Select “Insert”, "Select", “Update” and “Delete” for capabilities
- Corresponds to Create, Read, Update and Delete in CRUD operations
- Select targeted tables (in this case it will be the
locationstable that has a columncompanyidthat will relate to the filter I will create)- Select
locationstable as this is the table that "managers" will need to update
- Select
- Find table in your schema that will relate back to the locations table that also will include
roleids andcompanyidas they are essential to the policy filter- In this case we will use the child table
employee_collectioninside companies as it will contain both thecompanyidvia company- The roleid will be contained within the role class "managers" that is assigned to this rule.
- In this case we will use the child table
- Create policy filter
company.employees_collection ANY(roleid = $_PRINCIPAL.roleid).companyid- This policy filter will ensure that users
roleidhas thecompanyidmust match the columncompanyidin locations table
- This policy filter will ensure that users
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
locationsmanaged by employees withroleid = 1234. - The
ANYfilter ensures that theroleidmatches an employee inemployee_collectionwhosecompanyidaligns with thelocationstable. - Managers with
roleid = 1234can update the name of the location, as can employees withroleid = 9874androleid = 1111.
If a user with roleid = 2345 attempts this action, the filter will not return any matching results, preventing them from altering the location.