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 thecreate_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 theparents
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 thetarget
to theparents
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 theparents
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.parentid
↔parents.parentid
: Direct link to theparents
entity.
- Indirect Relationship:
target.parentid
↔child.parentid
: Sincechild
entities are nested within parents, thetarget
table can reference relatedchild
entities 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 toparents
and can indirectly access nestedchild
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.
- 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
ANY
operator 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_collection
correctly references its parent entity (e.g.,parent
field 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 theparentid
in the nested structure to theparentid
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
- 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
locations
table that has a columncompanyid
that will relate to the filter I will create)- Select
locations
table 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
roleid
s andcompanyid
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 thecompanyid
via 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
roleid
has thecompanyid
must match the columncompanyid
in 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
locations
managed by employees withroleid = 1234
. - The
ANY
filter ensures that theroleid
matches an employee inemployee_collection
whosecompanyid
aligns with thelocations
table. - Managers with
roleid = 1234
can update the name of the location, as can employees withroleid = 9874
androleid = 1111
.
If a user with roleid = 2345
attempts this action, the filter will not return any matching results, preventing them from altering the location.