DAY 18 WEEK 4: INTEGRATION

PDP Integration in Plugins

Implement fine-grained authorization in plugins using the AuthZEN API, batch evaluations, decision context, and PIP-enriched policies.

Authentication tells you who the user is. Authorization tells you what they can do. The PDP (Policy Decision Point) is your authorization engine—call it from plugins to make real-time permission decisions that adapt to context, relationships, and 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).

sequenceDiagram participant Plugin as Plugin participant BFF as BFF (bff.self) participant PDP as PDP participant PIP as PIP (Membership) participant Cache as Redis Cache Plugin->>BFF: sdk.authz.evaluate(request) BFF->>PDP: POST /access/v1/evaluation PDP->>Cache: Check cached decision alt Cache hit Cache-->>PDP: Cached decision else Cache miss PDP->>PIP: Fetch user capabilities PIP-->>PDP: User context PDP->>PDP: Evaluate policy PDP->>Cache: Store decision end PDP-->>BFF: Decision + Context BFF-->>Plugin: { decision: true/false, context: {...} }

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

Checking permissions one at a time is slow. If you have 10 documents with 3 actions each, that's 30 API calls. Batch evaluation sends all checks in a single request—3x faster, better UX.

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
      });
    })
  );
}
💡 Performance Impact

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;
}
⚠️ Cache Invalidation

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

The 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.
IGA MCP Tool → PDP Flow
flowchart TB subgraph Plugin["IGA Plugin / MCP Tool"] CG["IGA.CheckGuardrail"] CS["IGA.CheckSoD"] DB["IGA.DiscoverBirthright"] CT["IGA.CheckTemporal"] end subgraph PDP["PDP (identity-governance)"] EVAL["/access/v1/evaluation"] SEARCH["/access/v1/search/resource"] end CG -->|"resource.properties.
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

ToolPDP EndpointWhen to Use
IGA.CheckGuardrail/access/v1/evaluationBefore add_to_group, provision, assign_role
IGA.CheckSoD/access/v1/evaluationBefore role assignments
IGA.DiscoverBirthright/access/v1/search/resourceStart of Joiner workflows
IGA.CheckTemporal/access/v1/evaluationContractor/intern access validation
💡 Automatic Application Routing

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

1 What's the benefit of batch evaluation over individual checks?

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.

2 Why should you "fail closed" when authorization checks error?

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.

3 How do you get an explanation for why access was denied?

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
4 What does pdp_application in plugins.yaml control?

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.

5 When should you invalidate the client-side permission cache?

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
🎉 What You Learned

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.