Dynamic permissions
Dynamic permissions allow you to control who can execute or approve self-service actions based on data in your software catalog. Unlike static permissions, where you explicitly list roles, users, or teams, dynamic permissions let you define logic that evaluates at runtime.
This enables access control patterns such as requiring manager approval, restricting actions based on entity ownership, or enforcing separation of duties.
Common use cases
- Ensure that action executions requested by a team member can only be approved by his/her direct manager.
- Perform validations/manipulations on inputs that depend on data from related entities.
- Ensure that only those who are on-call can perform rollbacks of a service with issues.
- Allow service owners to modify their own infrastructure freely, but also enforce approval when they seek to make changes to infrastructure shared by multiple services.
How it works
Dynamic permissions are configured using a policy object in the action's permissions JSON.
The policy requires two keys:
queries- Fetch entities from your software catalog based on rules you define.conditions- JQ expressions that evaluate the query results.
Both keys must be present in the policy object.
When a policy is defined:
- The
roles,users, andteamskeys control who can see the action. - The
policyexclusively controls who can execute or approve the action.
When no policy is defined:
- The
roles,users, andteamskeys control both visibility and execution/approval.
Evaluation order
Dynamic permissions are evaluated after blueprint permissions. This means:
- Port first checks if the user has the required permissions on the underlying blueprint.
- Only if the blueprint permissions allow access, the dynamic permission policy is evaluated.
Dynamic permissions can only further restrict who can execute or approve an action, or dynamically determine approvers. They cannot bypass blueprint-level restrictions.
Configuration
Dynamic permissions are defined in the action's permissions JSON. This section covers how to access and structure the configuration.
Accessing the permissions JSON
To define dynamic permissions for an action:
- Go to the Self-service page of your portal.
- Hover over the desired action, click on the
...icon in its top-right corner, and chooseEdit. - Click on the
Edit JSONbutton in the top-right corner of the configuration modal, then choose thePermissionstab.
The policy object structure
The permissions JSON contains two top-level keys:
"execute"- defines who can run the action."approve"- defines who can approve the action (only relevant if manual approval is enabled).
Under each of these keys, you can define:
roles- which roles can execute/approve the action.users- which specific users can execute/approve the action.teams- which teams can execute/approve the action.policy- dynamic logic containing:
Note that to remove an existing policy, set "policy": null in the JSON configuration. Simply deleting the policy content will not remove it.
The following example shows the complete structure of a policy object:
Complete policy structure example (click to expand)
{
"execute": {
"policy": {
"queries": {
"query_name": {
"rules": [
// Your rule/s logic here
],
"combinator": "and"
}
},
"conditions": [
// A jq query resulting in a boolean value (allowed/not-allowed to execute)
]
}
},
"approve": {
"roles": [
"Admin"
],
"users": [],
"teams": [],
"policy": {
"queries": {
"query_name": {
"rules": [
// Your rule/s logic here
],
"combinator": "and"
}
},
"conditions": [
// A jq query resulting in an array of strings (a list of users who can approve the action)
]
}
}
}
How policy affects visibility vs execution
When no policy is defined, roles, users, and teams control both visibility and execution/approval permissions.
When a policy is defined, roles, users, and teams control only visibility, while the policy exclusively controls execution or approval.
Example without policy:
The action is visible and executable by users with the admin role or members of the engineering team:
"execute": {
"roles": ["admin"],
"users": [],
"teams": ["engineering"]
}
Example with policy:
Using the same configuration, but this time with a policy object defined, these roles and teams only determine who can view the action, while the policy exclusively controls who can execute or approve it.
In the following example, the action will be visible to admin and engineering team members, but its execution permissions depend only on whether the policy conditions evaluate to true:
"execute": {
"roles": ["admin"],
"users": [],
"teams": ["engineering"],
"policy": {
"queries": {
"example_query": {
"rules": [
// Your rule logic here
],
"combinator": "and"
}
},
"conditions": [
// A jq query returning a boolean (allowed/not-allowed to execute)
]
}
}
Condition return types
The conditions array in a policy behaves differently depending on whether it's used for execute or approve permissions: execute conditions return a boolean, while approve conditions return an array of email addresses.
- Execute conditions
- Approve conditions
Execute conditions must return a boolean value (true or false).
true→ the user can execute the action.false→ the user cannot execute the action.
Example condition:
"conditions": [
".results.search_entity.entities | length == 0"
]
This condition checks if the query returned zero entities. Execution is allowed only in that case.
Approve conditions must return an array of strings containing the email addresses of users who can approve the action.
- The array must contain email addresses, not user IDs. Fields like
createdByorupdatedByreturn user IDs, which will silently fail to match any approvers. - An empty array means no one can approve.
Example condition:
"conditions": [
"[.results.approvingUsers.entities[] | select(.relations.team == $executerTeam) | .identifier]"
]
This condition returns an array of user emails who are on the same team as the executor.
Available context variables
When writing JQ conditions, you have access to the action's trigger data - the same context available when defining backend payloads.
Commonly used in conditions:
| Variable | Description |
|---|---|
.trigger.user.email | Email of the user who triggered the action |
.inputs.<field_name> | Values provided by the user for action inputs |
.entity | The entity being acted on (DAY-2/DELETE operations) |
.results.<query_name>.entities | Array of entities returned by your queries |
Access query results using .results.<query_name>.entities. Each entity contains .identifier, .title, .properties.*, and .relations.*.
Troubleshooting
Dynamic permissions can be challenging to debug because there's limited visibility into what's happening at runtime. Here are strategies to diagnose common issues.
Common issues
Policy not working at all (click to expand)
Check blueprint permissions first. Dynamic permissions are evaluated after blueprint permissions. If the user doesn't have the required blueprint permissions, the policy won't even be evaluated.
To verify:
- Temporarily remove the
policyobject from the permissions JSON. - Test if the user can execute the action with just
roles/users/teams. - If they still can't, the issue is with blueprint permissions, not your policy.
No approvers appear for approval policy (click to expand)
This usually means your condition is returning user IDs instead of email addresses. Approve conditions must return an array of email addresses.
Common culprits:
- Using
.createdByor.updatedBy(these return user IDs, not emails) - Using
.identifieron a user entity when the identifier is an ID rather than an email
Fix: If your user entities use email as the identifier, use .identifier. If not, you need to access the email from a property, e.g., .properties.email.
JQ condition syntax errors (click to expand)
JQ syntax errors cause conditions to fail silently. Test your JQ expressions locally before adding them to Port.
Test JQ locally:
- Create a file with sample context data (e.g.,
test-data.json) - Run your expression:
jq '<your_expression>' test-data.json
Common JQ mistakes:
- Missing quotes around strings
- Using
=instead of==for comparison - Forgetting to handle empty arrays (use
// []for defaults)
Testing queries with the search API
You can test your query rules using Port's search API before adding them to a policy. This helps verify that your rules return the expected entities.
The query structure in dynamic permissions is identical to the search API request body:
// In your policy:
"queries": {
"my_query": {
"rules": [...],
"combinator": "and"
}
}
// Equivalent Search API request body:
{
"rules": [...],
"combinator": "and"
}
When testing, remember that template variables like {{ .inputs.name }} won't work in the API - you will need to replace them with actual values.
Limitations
- Each query can return up to 1000 entities. Make your queries as precise as possible to stay within this limit.
- Any query that fails to evaluate will be silently ignored.
- Dynamically resolved approvers are only notified via the Port UI. Email notifications are not sent to them. To send email notifications, define approvers statically using the
users,roles, orteamskeys. - There is no limit to the number of queries you can define per policy.
Examples
See the examples page for practical implementations of dynamic permissions patterns.