PDP Integration in Plugins
Implement fine-grained authorization in plugins using the AuthZEN API, batch evaluations, decision context, and PIP-enriched policies.
📋Learning Objectives
- Understand the AuthZEN 1.1 evaluation API
- Use
sdk.authz.evaluate()for single checks - Use
sdk.authz.batchEvaluate()for efficient multi-checks - Build IfCan and Protected components
- Request and display decision context/explanations
- Handle authorization failures gracefully
🏗️PDP Integration Architecture
Plugins call the PDP through the BFF proxy. The PDP evaluates policies and optionally fetches additional context from PIPs (Policy Information Points).
Key Concepts
| Concept | Description |
|---|---|
| Subject | Who is requesting access (user, service, AI agent) |
| Resource | What they're accessing (document, role, API) |
| Action | What they want to do (view, edit, delete, assign) |
| Context | Additional conditions (time, location, risk score) |
| Decision | Permit or Deny (boolean) |
🔐Single Permission Check
The most common pattern—check if a user can perform a specific action on a specific resource.
Using sdk.authz.evaluate()
// src/components/DocumentActions.tsx
const React = (globalThis as any).React;
const { useState, useEffect } = React;
const sdk = (globalThis as any).pluginSdk;
export function DocumentActions({ documentId }: { documentId: string }) {
const [canEdit, setCanEdit] = useState(false);
const [canDelete, setCanDelete] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkPermissions();
}, [documentId]);
const checkPermissions = async () => {
try {
// Check edit permission
const editResult = await sdk.authz.evaluate({
resource: { type: 'document', id: documentId },
action: { name: 'edit' }
});
setCanEdit(editResult.decision === true);
// Check delete permission
const deleteResult = await sdk.authz.evaluate({
resource: { type: 'document', id: documentId },
action: { name: 'delete' }
});
setCanDelete(deleteResult.decision === true);
} catch (err) {
console.error('Permission check failed:', err);
// Fail closed - deny on error
setCanEdit(false);
setCanDelete(false);
} finally {
setLoading(false);
}
};
if (loading) {
return React.createElement('div', null, 'Checking permissions...');
}
return React.createElement('div', { style: actionsStyle },
canEdit && React.createElement('button', {
onClick: () => handleEdit(documentId),
style: buttonStyle
}, '✏️ Edit'),
canDelete && React.createElement('button', {
onClick: () => handleDelete(documentId),
style: { ...buttonStyle, ...dangerStyle }
}, '🗑️ Delete')
);
}
AuthZEN Request Format
// Full request structure (SDK builds this for you)
{
"subject": {
"type": "user",
"id": "user-12345" // Auto-filled from session
},
"resource": {
"type": "document",
"id": "doc-abc123"
},
"action": {
"name": "edit"
},
"context": {
"reason_user": true // Request explanation
}
}
// Response
{
"decision": true,
"context": {
"reason_user": {
"en": "User has 'editor' role on this document"
}
}
}
📦Batch Permission Checks
Using sdk.authz.batchEvaluate()
// Check multiple permissions at once
export function DocumentList({ documents }: { documents: Document[] }) {
const [permissions, setPermissions] = useState<Map<string, any>>(new Map());
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAllPermissions();
}, [documents]);
const checkAllPermissions = async () => {
if (documents.length === 0) {
setLoading(false);
return;
}
// Build evaluation requests for all documents
const evaluations = documents.flatMap(doc => [
{
resource: { type: 'document', id: doc.id },
action: { name: 'view' }
},
{
resource: { type: 'document', id: doc.id },
action: { name: 'edit' }
},
{
resource: { type: 'document', id: doc.id },
action: { name: 'delete' }
}
]);
try {
// Single API call for all permissions!
const results = await sdk.authz.batchEvaluate(evaluations);
// Map results back to documents
const permMap = new Map();
let idx = 0;
documents.forEach(doc => {
permMap.set(doc.id, {
view: results.decisions[idx++]?.decision === true,
edit: results.decisions[idx++]?.decision === true,
delete: results.decisions[idx++]?.decision === true
});
});
setPermissions(permMap);
} catch (err) {
console.error('Batch permission check failed:', err);
} finally {
setLoading(false);
}
};
// Use permissions in rendering
return React.createElement('div', null,
documents.map(doc => {
const perms = permissions.get(doc.id) || {};
if (!perms.view) return null; // Hide if can't view
return React.createElement(DocumentCard, {
key: doc.id,
document: doc,
canEdit: perms.edit,
canDelete: perms.delete
});
})
);
}
Individual checks: 10 docs × 3 actions = 30 API calls × 50ms = 1500ms
Batch check: 1 API call × 80ms = 80ms
Result: 18x faster!
🎨IfCan Component Pattern
A reusable component for declarative permission-based rendering.
// src/components/IfCan.tsx
const React = (globalThis as any).React;
const { useState, useEffect } = React;
const sdk = (globalThis as any).pluginSdk;
interface IfCanProps {
resource: string;
resourceId?: string;
action: string;
children: React.ReactNode;
fallback?: React.ReactNode;
onDenied?: () => void;
}
export function IfCan({
resource,
resourceId,
action,
children,
fallback = null,
onDenied
}: IfCanProps) {
const [authorized, setAuthorized] = useState<boolean | null>(null);
useEffect(() => {
checkPermission();
}, [resource, resourceId, action]);
const checkPermission = async () => {
try {
const result = await sdk.authz.evaluate({
resource: { type: resource, id: resourceId },
action: { name: action }
});
const allowed = result.decision === true;
setAuthorized(allowed);
if (!allowed && onDenied) {
onDenied();
}
} catch (err) {
console.error('IfCan check failed:', err);
setAuthorized(false); // Fail closed
}
};
// Still loading
if (authorized === null) {
return null;
}
return authorized ? children : fallback;
}
// Usage
function Dashboard() {
return React.createElement('div', null,
React.createElement('h1', null, 'Dashboard'),
// Admin panel - hidden if no permission
React.createElement(IfCan, {
resource: 'admin-panel',
action: 'view'
},
React.createElement(AdminPanel, null)
),
// Reports - show message if denied
React.createElement(IfCan, {
resource: 'reports',
action: 'view',
fallback: React.createElement('p', null, 'You cannot view reports')
},
React.createElement(ReportsWidget, null)
)
);
}
💬Decision Context & Explanations
The PDP can explain why a decision was made. This is useful for debugging, auditing, and user feedback.
Requesting Explanations
// Request with explanation
const result = await sdk.authz.evaluate({
resource: { type: 'document', id: 'doc-123' },
action: { name: 'edit' },
context: {
reason_user: true, // Human-readable explanation
reason_admin: true // Technical details for admins
}
});
console.log(result);
// {
// "decision": false,
// "context": {
// "reason_user": {
// "en": "You do not have the 'editor' role for this document"
// },
// "reason_admin": {
// "policy": "document-access-policy",
// "rule": "require-editor-role",
// "missing_condition": "subject.roles contains 'doc-editor'",
// "subject_roles": ["viewer"],
// "required_roles": ["editor", "admin"]
// }
// }
// }
Displaying Denial Reasons
// Component that shows why access was denied
export function AccessDeniedMessage({ resource, action }: any) {
const [reason, setReason] = useState<string>('');
useEffect(() => {
fetchReason();
}, [resource, action]);
const fetchReason = async () => {
const result = await sdk.authz.evaluate({
resource: { type: resource.type, id: resource.id },
action: { name: action },
context: { reason_user: true }
});
if (!result.decision && result.context?.reason_user?.en) {
setReason(result.context.reason_user.en);
}
};
return React.createElement('div', { style: deniedStyle },
React.createElement('h3', null, '🔒 Access Denied'),
React.createElement('p', null,
reason || 'You do not have permission for this action.'
),
React.createElement('button', {
onClick: () => requestAccess(resource),
style: requestButtonStyle
}, 'Request Access')
);
}
📊Context Enrichment from PIPs
Policies can reference external context from PIPs (Policy Information Points). The Membership service provides rich user context.
Available PIP Context
| PIP Endpoint | Data Provided |
|---|---|
/pip/capabilities |
User's capabilities at a location |
/pip/data-scope |
Data access boundaries |
/pip/step-up |
MFA/step-up requirements |
/pip/chain-eligibility |
AI delegation chain validation |
Policy Using PIP Context
# Example policy rule using PIP data
rules:
- id: require-manager-approval
description: "Expenses over $10k need manager capability"
condition: |
request.resource.type == 'expense' &&
request.resource.properties.amount > 10000
effect: permit
when:
- pip.capabilities contains 'approve-large-expenses'
- pip.data_scope.max_approval >= request.resource.properties.amount
Passing Custom Context
// Pass additional context to influence decision
const result = await sdk.authz.evaluate({
resource: {
type: 'expense',
id: 'exp-456',
properties: {
amount: 15000,
department: 'engineering'
}
},
action: { name: 'approve' },
context: {
// Custom context for policies
current_location: 'office',
device_trust_level: 'high',
time_of_day: new Date().toISOString()
}
});
⚡Caching Strategies
Authorization decisions can be cached to improve performance, but with care.
Client-Side Permission Cache
// Simple permission cache with TTL
class PermissionCache {
private cache = new Map<string, { decision: boolean; expires: number }>();
private ttlMs: number;
constructor(ttlSeconds: number = 60) {
this.ttlMs = ttlSeconds * 1000;
}
private getKey(resource: string, resourceId: string | undefined, action: string): string {
return `${resource}:${resourceId || '*'}:${action}`;
}
get(resource: string, resourceId: string | undefined, action: string): boolean | null {
const key = this.getKey(resource, resourceId, action);
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() > entry.expires) {
this.cache.delete(key);
return null;
}
return entry.decision;
}
set(resource: string, resourceId: string | undefined, action: string, decision: boolean): void {
const key = this.getKey(resource, resourceId, action);
this.cache.set(key, {
decision,
expires: Date.now() + this.ttlMs
});
}
invalidate(resource?: string): void {
if (!resource) {
this.cache.clear();
} else {
for (const key of this.cache.keys()) {
if (key.startsWith(resource + ':')) {
this.cache.delete(key);
}
}
}
}
}
// Usage with cache
const permissionCache = new PermissionCache(60); // 60 second TTL
async function checkWithCache(resource: string, resourceId: string, action: string) {
// Check cache first
const cached = permissionCache.get(resource, resourceId, action);
if (cached !== null) {
return cached;
}
// Call PDP
const result = await sdk.authz.evaluate({
resource: { type: resource, id: resourceId },
action: { name: action }
});
// Cache result
const decision = result.decision === true;
permissionCache.set(resource, resourceId, action, decision);
return decision;
}
Permissions can change! Invalidate cache when:
- User's role changes
- Resource ownership changes
- User logs out/in
- After time-based policy window
When in doubt, use shorter TTLs (30-60 seconds).
🛡️IGA Plugin Integration Pattern
identity-governance PDP application adds provisioning governance to plugins. Instead of checking "can user view a document?", IGA plugins check "can this contractor be provisioned into an admin group?" using dedicated MCP tools that wrap PDP calls.
pdp_application:
identity-governance"| EVAL CS --> EVAL DB --> SEARCH CT --> EVAL
IGA MCP Tool Example: CheckGuardrail
// IGA tools automatically set pdp_application: "identity-governance"
const result = await tools.execute("IGA.CheckGuardrail", {
employee_type: "Contractor",
action: "add_to_group",
target_type: "group",
target_id: "IT-Admins"
});
// Decision flow based on result
if (result.blocked) {
// Show user: result.user_message
// Show remediation: result.remediation
} else if (result.approval_required) {
// Trigger approval workflow
} else {
// Proceed with provisioning
}
Available IGA MCP Tools
| Tool | PDP Endpoint | When to Use |
|---|---|---|
IGA.CheckGuardrail | /access/v1/evaluation | Before add_to_group, provision, assign_role |
IGA.CheckSoD | /access/v1/evaluation | Before role assignments |
IGA.DiscoverBirthright | /access/v1/search/resource | Start of Joiner workflows |
IGA.CheckTemporal | /access/v1/evaluation | Contractor/intern access validation |
All IGA MCP tools automatically set pdp_application: "identity-governance" in the resource properties. You don't need to specify it — the tool handles routing to the correct policy set.
🔧Troubleshooting
Issue: All permissions returning false
# Check if plugin has PDP permission in plugins.yaml
permissions:
api:
- method: POST
path: /access/v1/evaluation
- method: POST
path: /access/v1/evaluations # For batch
# Verify PDP application matches policy
pdp_application: your-plugin-app # Must exist in PDP
Issue: Batch evaluation fails
# Check batch size limit (default: 100)
# Split large batches into chunks
const BATCH_SIZE = 50;
const chunks = [];
for (let i = 0; i < evaluations.length; i += BATCH_SIZE) {
chunks.push(evaluations.slice(i, i + BATCH_SIZE));
}
const allResults = await Promise.all(
chunks.map(chunk => sdk.authz.batchEvaluate(chunk))
);
# Flatten results
const decisions = allResults.flatMap(r => r.decisions);
Issue: Getting "policy not found" error
# Verify pdp_application in plugins.yaml matches PDP config
pdp_application: role-viewer-app
# Check PDP has policies for this application
curl -k https://pdp.self.empowernow.ai/access/v1/evaluation \
-H "Authorization: Bearer $TOKEN"
# Verify resource type exists in policies
# Request uses: resource: { type: 'document', ... }
# Policy must define rules for resource type 'document'
Issue: Decision doesn't match expected
# Add X-Debug header for detailed evaluation trace
const response = await sdk.api.fetch('/access/v1/evaluation', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Debug': 'true' # Enable debug output
},
body: JSON.stringify({
resource: { type: 'document', id: 'doc-123' },
action: { name: 'edit' },
context: { reason_admin: true } # Get detailed reason
})
});
# Response includes evaluation trace
# Check context.reason_admin for policy evaluation details
❓Knowledge Check
Batch evaluation sends all permission checks in a single API call instead of multiple calls. For 10 resources × 3 actions, that's 1 call vs 30 calls—up to 18x faster and much better UX.
If an authorization check fails (network error, timeout), defaulting to deny is the secure choice. "Failing open" (allowing on error) would be a security vulnerability—an attacker could trigger errors to bypass authorization.
Include reason_user: true in the request context:
const result = await sdk.authz.evaluate({
resource: { type: 'doc', id: '123' },
action: { name: 'edit' },
context: { reason_user: true }
});
// result.context.reason_user.en has explanation
It specifies which policy application scope the plugin uses. The PDP looks up policies for this application when evaluating requests. It must match an application defined in the PDP's policy configuration.
Invalidate when permissions might have changed:
- User's role is assigned/revoked
- Resource ownership changes
- User logs out and back in
- After receiving SSE events about role changes
Use short TTLs (30-60s) if unsure about invalidation timing.
📝Day 18 Checkpoint
- Understand AuthZEN request/response format
- Can use sdk.authz.evaluate() for single checks
- Can use sdk.authz.batchEvaluate() for efficient multi-checks
- Built IfCan component for declarative rendering
- Know how to request and display decision explanations
- Understand PIP context enrichment
- Can implement client-side permission caching
You now know how to integrate fine-grained authorization into your plugins. The PDP provides real-time, context-aware decisions that go far beyond simple role checks—enabling dynamic policies based on location, time, relationships, and more.