Policy Decision Point (PDP) - Hands-on
Make authorization requests to PDP, understand decision explanations, and explore Policy Information Points (PIPs).
📋Learning Objectives
- Make authorization requests to the PDP evaluation endpoint
- Understand request and response structure
- Interpret decision explanations
- Perform batch evaluations for UI permission checks
- Use search endpoints including
/search/resourcefor birthright discovery - Explore Policy Information Points (PIPs)
- Make IGA requests: guardrail checks, SoD detection, and birthright discovery
- Simulate a complete Joiner workflow using PDP
🔬Anatomy of an Authorization Decision
(who)"] A["Action
(what)"] R["Resource
(on what)"] C["Context
(when/where)"] end S --> RP A --> RP R --> RP C --> RP subgraph PDP["🧠 PDP Engine"] RP["Request Parser"] RP --> PL["Policy Loader"] PL --> |"Load by app"| PE["Policy Evaluator"] PE --> |"Need attributes?"| PIP["PIPs"] PIP --> |"roles, metadata"| PE PE --> DM["Decision Maker"] end DM --> D{Decision} D -->|"✅"| PERMIT["PERMIT
+ Explanation"] D -->|"❌"| DENY["DENY
+ Explanation"] style PDP fill:#7c3aed,color:#fff style PERMIT fill:#059669,color:#fff style DENY fill:#e11d48,color:#fff
Decision Logic Priority
When multiple rules match, the PDP follows this priority:
- Explicit Deny: Any matching deny rule wins (fail-closed)
- Explicit Permit: If no deny, check for permit rules
- Default Effect: If nothing matches, use the application's default (usually deny)
The "deny wins" rule is a security feature. Even if 10 rules say permit, a single deny overrides them all. This makes it safer to add restrictions—you don't have to worry about accidentally leaving a backdoor open.
🔧Basic Evaluation Request
Make a simple authorization request to the PDP:
# Basic evaluation request
curl -k -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-d '{
"subject": { "type": "user", "id": "user-123" },
"action": { "name": "read" },
"resource": { "type": "document", "id": "doc-456" }
}'
Expected Response (Permit)
{ "decision": true }
Expected Response (Deny)
{ "decision": false }
$body = @{
subject = @{ type = "user"; id = "user-123" }
action = @{ name = "read" }
resource = @{ type = "document"; id = "doc-456" }
} | ConvertTo-Json -Depth 3
Invoke-RestMethod -Uri "https://pdp.self.empowernow.ai/access/v1/evaluation" `
-Method POST -Body $body -ContentType "application/json" -SkipCertificateCheck
📊Request with Subject Properties
Include additional subject and resource attributes for more precise authorization:
Copy curl and run in your terminal. This request includes roles, department, and clearance level. Policies can use any of these for decisions.
Common Subject Properties
| Property | Use Case |
|---|---|
roles | Role-based access control (RBAC) |
department | Department-level restrictions |
clearance_level | Security classification matching |
location | Geo-based access control |
You can pass properties in the request, OR let PIPs fetch them dynamically. PIPs are better when you want policies to always use current data (e.g., user's current roles from the database).
💬Exercise 3: Get Decision Explanations
Request detailed explanations for why a decision was made:
curl -k -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-d '{
"subject": { "type": "user", "id": "user-123" },
"action": { "name": "delete" },
"resource": { "type": "document", "id": "doc-456" },
"context": { "reason_admin": true, "reason_user": true }
}'
Response with Explanation
{
"decision": false,
"context": {
"reason_admin": {
"explanation": "Access denied: User lacks 'admin' role required for delete action",
"matched_rule": "deny-delete-non-admin",
"policy_file": "documents.yaml"
},
"reason_user": { "explanation": "You don't have permission to delete this document" }
}
}
- reason_admin: Technical details for debugging
- reason_user: User-friendly message for UI
📦Batch Evaluations
Evaluate multiple authorization requests in a single call:
Copy curl and run in your terminal. This batch checks read, write, and delete permissions for a single user on a document.
When to Use Batch
- Page Load: Check all permissions needed for a page's UI elements
- List Views: Check permissions for each item in a list
- Menu Rendering: Determine which menu items to show
Batch up to 100 evaluations in a single request. The PDP optimizes by sharing policy lookups across evaluations with the same application context.
🔍Search for Allowed Actions
Find what actions a user can perform on a resource:
Copy curl and run in your terminal. Instead of asking "can user do X?", this asks "what can user do?"
Search Endpoints
| Endpoint | Question Answered | Use Case |
|---|---|---|
/access/v1/search | What actions can subject perform on this resource? | Dynamic UIs, menu rendering |
/access/v1/search/resource | What resources can subject access with this action? | Birthright discovery, resource listing |
/access/v1/search/subject | Who can perform this action on this resource? | Access reviews, auditing |
subject access?"| Result1["Target systems,
documents, groups"] SS -->|"Who can perform
action on resource?"| Result2["Users, services, roles"] SA -->|"What actions can
subject perform?"| Result3["read, write, delete,
birthright_provision"]
The /search/resource endpoint is used by IGA.DiscoverBirthright to find target systems for new hires. Instead of asking "can user view this page?", it asks "what target systems should this Engineering FTE be provisioned to?"
🔌Policy Information Points (PIPs)
Built-in PIPs
| PIP | Data Source | Example Attributes |
|---|---|---|
| MembershipPIP | Membership Service | roles, groups, org hierarchy |
| ResourcePIP | Application databases | owner, classification, department |
| TimePIP | System clock | current_time, day_of_week, is_business_hours |
| GeoIPPIP | IP geolocation service | country, region, is_vpn |
Why PIPs Matter
- Always Current: Roles checked at decision time, not token-issue time
- Simpler Requests: Don't need to pass all attributes—PIP fetches them
- Central Control: Change role assignments, decisions update immediately
PIPs make policies dynamic. A user might have "admin" in their JWT, but if the Membership service says they were demoted 5 minutes ago, the PIP-fetched role wins. This is why EmpowerNow uses PIPs—security shouldn't depend on stale tokens.
🧪Application-Scoped Requests
pdp_application field in resource.properties tells the PDP which policy namespace to use.
Scope a request to a specific application using resource.properties.pdp_application:
# IT Shop: Can user checkout their cart?
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-d '{
"subject": { "type": "user", "id": "user-123" },
"action": { "name": "checkout" },
"resource": {
"type": "cart", "id": "cart-789",
"properties": { "pdp_application": "it-shop" }
}
}' | jq .
# IGA: Can contractor join admin group?
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-d '{
"subject": { "type": "user", "id": "contractor@acme.com",
"properties": { "employee_type": "Contractor" } },
"action": { "name": "add_to_group" },
"resource": {
"type": "group", "id": "IT-Admins",
"properties": { "name": "IT-Admins", "pdp_application": "identity-governance" }
}
}' | jq .
The X-Application header is still accepted, but resource.properties.pdp_application is the canonical approach. All new integrations should use the properties field. Missing values default to "global".
🛡️IGA Authorization Requests
Copy curl and run in your terminal. A contractor tries to join an admin group. The guardrail policy should block this and return remediation advice.
{
"decision": false,
"context": {
"guardrail_id": "GR-001",
"reason_user": {"en": "Contractors cannot be added to admin groups"},
"remediation": "Remove admin group request or convert contractor to FTE"
}
}
Copy curl and run in your terminal. A payment approver requests the vendor_manager role. SoD policy detects a toxic combination (fraud risk).
{
"decision": false,
"context": {
"sod_violation": "SOD-001",
"risk_level": "critical",
"toxic_combination": true,
"reason_user": {"en": "SoD conflict: Payment Approval + Vendor Management"},
"remediation": "Remove payment approval roles before requesting vendor management"
}
}
Copy curl and run in your terminal. An Engineering FTE is joining. The birthright search discovers which target systems to provision.
{
"results": [
{ "type": "target_system", "id": "av_entraid",
"properties": { "birthright_groups": ["All-Employees", "Engineering-All"],
"licenses": ["Microsoft 365 E3"] } },
{ "type": "target_system", "id": "av_ad_addomain",
"properties": { "birthright_groups": ["Engineering-Users", "VPN-Access"] } },
{ "type": "target_system", "id": "av_openldap",
"properties": { "birthright_groups": ["engineers", "developers"] } }
]
}
🔬Hands-on Labs
Create a reusable script for checking permissions from the command line.
Create a check-permission.sh script:
#!/bin/bash
# check-permission.sh - Check if user can perform action on resource
USER_ID=${1:-"user-123"}
ACTION=${2:-"read"}
RESOURCE_TYPE=${3:-"document"}
RESOURCE_ID=${4:-"doc-456"}
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-d "{
\"subject\": {\"type\": \"user\", \"id\": \"$USER_ID\"},
\"action\": {\"name\": \"$ACTION\"},
\"resource\": {\"type\": \"$RESOURCE_TYPE\", \"id\": \"$RESOURCE_ID\"},
\"context\": {\"reason_admin\": true}
}" | jq .
Make it executable and test:
chmod +x check-permission.sh
# Test: Can user-123 read document doc-456?
./check-permission.sh user-123 read document doc-456
# Test: Can user-123 delete document doc-456?
./check-permission.sh user-123 delete document doc-456
You should see JSON responses with decision and context.reason_admin showing why.
Simulate and debug a real authorization failure.
Simulate a denied request:
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-H "X-Application: it-shop" \
-d '{
"subject": {"type": "user", "id": "guest"},
"action": {"name": "admin_panel"},
"resource": {"type": "system", "id": "settings"},
"context": {"reason_admin": true, "reason_user": true}
}' | jq .
Analyze the explanation:
- What rule denied the request?
- What policy file contains the rule?
- What would the user-facing message say?
Test with an admin user:
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-H "X-Application: it-shop" \
-d '{
"subject": {
"type": "user",
"id": "admin-user",
"properties": {"roles": ["system-admin"]}
},
"action": {"name": "admin_panel"},
"resource": {"type": "system", "id": "settings"}
}' | jq .
- How does the decision change when you add the admin role?
- What if you add roles but not
system-admin?
Simulate what a frontend would do on page load to determine which UI elements to show.
Create a batch request for a product detail page:
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluations \
-H "Content-Type: application/json" \
-H "X-Application: it-shop" \
-d '{
"evaluations": [
{"subject": {"type": "user", "id": "user-123"}, "action": {"name": "view"}, "resource": {"type": "product", "id": "prod-1"}},
{"subject": {"type": "user", "id": "user-123"}, "action": {"name": "edit"}, "resource": {"type": "product", "id": "prod-1"}},
{"subject": {"type": "user", "id": "user-123"}, "action": {"name": "delete"}, "resource": {"type": "product", "id": "prod-1"}},
{"subject": {"type": "user", "id": "user-123"}, "action": {"name": "add_to_cart"}, "resource": {"type": "product", "id": "prod-1"}}
]
}' | jq .
Map decisions to UI elements:
| Action | UI Element | If False |
|---|---|---|
| view | Product details | Show "Access Denied" page |
| edit | Edit button | Hide or disable button |
| delete | Delete button | Hide or disable button |
| add_to_cart | Add to Cart button | Show "Contact sales" |
One batch request returns all permissions needed for the page. The frontend uses this to render the appropriate UI without making multiple round-trips.
Explore the identity-governance PDP application policies to understand guardrails, SoD, and birthright formats.
Navigate to the IGA policy directory:
# macOS/Linux
cd <YourRepoRoot>/serviceconfigs/pdp/config/policies/applications/identity-governance
ls -R
# Windows (PowerShell)
cd <YourRepoRoot>\serviceconfigs\pdp\config\policies\applications\identity-governance
Get-ChildItem -Recurse
You should see: principal-policy.json, guardrails/, sod/, birthright/, temporal/
Read a guardrail policy:
# macOS/Linux
cat guardrails/contractor-restrictions.json | python3 -m json.tool
# Windows
Get-Content guardrails\contractor-restrictions.json | ConvertFrom-Json | ConvertTo-Json -Depth 10
Find the denyIf block, the conditions array, and the on_deny.context with guardrail_id.
Read a birthright policy:
# macOS/Linux
cat birthright/engineering-birthright.json | python3 -m json.tool
Find the allowIf block and the on_permit.context with birthright_groups and naming_system.
- What resource type does GR-001 match on? (answer:
group) - What two role sets create the SOD-001 conflict?
- What target systems does BR-ENG-001 provision to?
Walk through the three-step IGA Joiner workflow: discover birthright systems, check guardrails, check SoD.
Step 1: Discover birthright systems for an Engineering FTE:
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/search/resource \
-H "Content-Type: application/json" \
-d '{
"subject": {"type": "user", "id": "newhire@acme.com",
"properties": {"employee_type": "Full-Time", "Department": "Engineering"}},
"action": {"name": "birthright_provision"},
"resource": {"type": "target_system",
"properties": {"pdp_application": "identity-governance"}}
}' | jq .
Note the systems returned (e.g., av_entraid, av_ad_addomain, av_openldap).
Step 2: Check guardrail for provisioning to each system:
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-d '{
"subject": {"type": "user", "id": "newhire@acme.com",
"properties": {"employee_type": "Full-Time"}},
"action": {"name": "provision"},
"resource": {"type": "target_system", "id": "av_entraid",
"properties": {"pdp_application": "identity-governance"}}
}' | jq .
Step 3: Check SoD before assigning roles:
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Content-Type: application/json" \
-d '{
"subject": {"type": "user", "id": "newhire@acme.com",
"properties": {"current_roles": ["Engineering-Users"]}},
"action": {"name": "acquire_role"},
"resource": {"type": "role", "id": "Engineering-Lead",
"properties": {"pdp_application": "identity-governance"}}
}' | jq .
- FTE gets 3 systems (all guardrails pass)
- Contractor would get 2 systems (AD blocked by BR-ENG-005 guardrail)
- Role assignment proceeds if no SoD conflict exists
- What changes if you switch
employee_typeto"Contractor"? - What would happen if the new hire already had
payment_approverand requestedvendor_manager?
🔧Troubleshooting Authorization
Every request returns deny, even for actions that should be permitted. No specific rule is matched in the explanation.
- Check if
X-Applicationheader orcontext.applicationis set - Without an application, PDP can't find the right policies
# Missing application - hits global default deny
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-d '{"subject": ..., "action": ..., "resource": ...}'
# With application - finds correct policies
curl -s -X POST https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "X-Application: it-shop" \
-d '{"subject": ..., "action": ..., "resource": ...}'
Always include the application context. Add it to your API gateway or include it in every request body.
User was just granted a new role, but authorization still fails. The JWT shows the old roles.
- Are you passing roles in the request body from the JWT?
- Or is PDP using MembershipPIP to fetch current roles?
- Check the decision explanation—what roles does it see?
- Use MembershipPIP for real-time role checks (recommended)
- Or have user re-authenticate to get a new token
- Or shorten token lifetime for faster role updates
A role assignment workflow silently fails. The user gets "request denied" with no obvious reason — they have no deny rules in their application policies.
- Check
reason_adminin the decision context — look forsod_violation - The user already holds a conflicting role from a previous assignment
- SoD policies in
identity-governanceblock toxic combinations across roles
# The deny context reveals the SoD conflict
"context": {
"sod_violation": "SOD-001",
"risk_level": "critical",
"reason_admin": {"en": "Payment approver cannot also be vendor manager"}
}
- Remove the conflicting role before assigning the new one
- Or request an SoD exception through the governance workflow
- Use
IGA.CheckSoDbefore role assignments to catch conflicts early
Admin users are getting denied for actions they should definitely have. The explanation shows a deny rule you didn't expect.
- Check global policies for broad deny rules
- Remember: deny wins, even over specific permits
- Look for
subjects: [{type: any}]in deny rules
# This deny rule blocks EVERYONE, including admins!
- id: deny-weekend-access
effect: deny
subjects:
- type: any # Oops - should exclude admins
conditions:
- attribute: context.day_of_week
operator: in
value: ["saturday", "sunday"]
Add exclusions to deny rules, or restructure to use permit rules with specific conditions instead of broad denies.
❓Knowledge Check
/evaluation (singular): Evaluates ONE authorization request. Returns a single decision.
/evaluations (plural): Evaluates MULTIPLE requests in a batch. Returns an array of decisions.
Use batch when checking permissions for UI elements or list items to reduce network overhead.
Deny wins. This is called "deny-override" and is a security feature.
Even if 10 rules say permit, a single matching deny rule will block access. This makes it safe to add restrictions without worrying about conflicting permits.
reason_admin: For developers and operations. Contains technical details like rule names and policy files. Never expose to end users.
reason_user: For end users. Contains human-readable messages like "You don't have permission to delete this document."
A Policy Information Point (PIP) fetches additional attributes during policy evaluation.
Example: MembershipPIP queries the current roles for a user from the database.
Why important: Ensures decisions use real-time data, not stale JWT claims. A role revoked 5 minutes ago is immediately effective.
It tells the PDP which policy namespace to use. Different applications have different policy sets.
Without an application context, the PDP falls back to global policies only—which typically default to deny.
Instead of asking "can user do X?", search asks "what can user do?"
Use cases:
- Building dynamic UIs that show only permitted actions
- Auditing what a user has access to
- Discovering available actions on a resource type
Pass in request when:
- Data is already available (from JWT or session)
- PIP would add latency you can't afford
- Attribute is request-specific (e.g., IP address)
Use PIPs when:
- Data changes frequently (roles, group membership)
- Data isn't in the calling application (resource metadata)
- You want centralized, real-time attribute lookup
Guardrails block individual provisioning actions based on a single rule and target. Example: "Contractors cannot join admin groups" — one subject attribute, one resource pattern.
SoD (Separation of Duties) detects conflicts between multiple existing/requested roles. Example: "A payment approver cannot also be a vendor manager" — requires checking current_roles against the requested role.
Standard search (/access/v1/search) asks "what actions can user do on this resource?"
Birthright discovery uses /access/v1/search/resource to ask "what target systems should this user be provisioned to?" based on HR attributes like employee_type and Department. The response includes on_permit.context with birthright_groups, naming_system, and licenses.
pdp_application is missing from a request?
▼
The PDP defaults to the "global" application, which typically only has baseline deny policies.
If the pdp_application value has an invalid format, the PDP returns HTTP 400. If the application ID is unknown/unregistered, the PDP returns HTTP 422 with a warning log.
📝Day 9 Checkpoint
- Made basic evaluation requests to PDP
- Included subject and resource properties in requests
- Retrieved and interpreted decision explanations
- Performed batch evaluations
- Used search endpoint including
/search/resourcefor birthright - Understand what PIPs are and how they work
- Made IGA requests: guardrail check, SoD check, birthright discovery
- Explored IGA policy files and understand the JSON format
- Simulated a Joiner workflow end-to-end
For the JSON IR schema, IdP DCR policy examples, and how to register new applications, use the official EmpowerNow documentation together with your platform administrator.