diff --git a/README.md b/README.md index 5b48326..f12476e 100644 --- a/README.md +++ b/README.md @@ -4,30 +4,44 @@ Temporal.io Worker Interceptor for Predicate Authority Zero-Trust authorization. This package provides a pre-execution security gate for all Temporal Activities, enforcing cryptographic authorization mandates before any activity code runs. -## Prerequisites +## Sidecar Prerequisite -This package requires the **Predicate Authority Sidecar** daemon to be running. The sidecar is a lightweight Rust binary that handles policy evaluation and mandate signing. +This package requires the **Predicate Authority Sidecar** daemon to be running. The sidecar is a high-performance Rust binary that handles policy evaluation and mandate signing locally—no data leaves your infrastructure. | Resource | Link | |----------|------| -| Sidecar Repository | [github.com/PredicateSystems/predicate-authority-sidecar](https://github.com/PredicateSystems/predicate-authority-sidecar) | +| Sidecar Repository | [predicate-authority-sidecar](https://github.com/PredicateSystems/predicate-authority-sidecar) | | Download Binaries | [Latest Releases](https://github.com/PredicateSystems/predicate-authority-sidecar/releases) | | License | MIT / Apache 2.0 | ### Quick Sidecar Setup +**Option A: Docker (Recommended)** ```bash -# Download the latest release for your platform -# Linux x64, macOS x64/ARM64, Windows x64 available +docker run -d -p 8787:8787 ghcr.io/predicatesystems/predicate-authorityd:latest +``` -# Extract and run -tar -xzf predicate-authorityd-*.tar.gz +**Option B: Download Binary** +```bash +# macOS (Apple Silicon) +curl -fsSL https://github.com/PredicateSystems/predicate-authority-sidecar/releases/latest/download/predicate-authorityd-darwin-arm64.tar.gz | tar -xz chmod +x predicate-authorityd +./predicate-authorityd --port 8787 --policy-file policy.json -# Start with a policy file +# Linux x64 +curl -fsSL https://github.com/PredicateSystems/predicate-authority-sidecar/releases/latest/download/predicate-authorityd-linux-x64.tar.gz | tar -xz +chmod +x predicate-authorityd ./predicate-authorityd --port 8787 --policy-file policy.json ``` +See [all platform binaries](https://github.com/PredicateSystems/predicate-authority-sidecar/releases) for Linux ARM64, macOS Intel, and Windows. + +**Verify it's running:** +```bash +curl http://localhost:8787/health +# {"status":"ok"} +``` + ## Installation ```bash diff --git a/examples/demo/Dockerfile.demo b/examples/demo/Dockerfile.demo new file mode 100644 index 0000000..ebb88b7 --- /dev/null +++ b/examples/demo/Dockerfile.demo @@ -0,0 +1,21 @@ +# Demo application container +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Copy pyproject.toml and install dependencies +COPY pyproject.toml ./ +RUN pip install --no-cache-dir . + +# Copy source code +COPY src/ ./src/ +COPY examples/demo/ ./examples/demo/ + +# Set Python path +ENV PYTHONPATH=/app/src + +# Run the demo +CMD ["python", "examples/demo/demo.py"] diff --git a/examples/demo/Dockerfile.sidecar b/examples/demo/Dockerfile.sidecar new file mode 100644 index 0000000..8cde591 --- /dev/null +++ b/examples/demo/Dockerfile.sidecar @@ -0,0 +1,25 @@ +# Predicate Authority Sidecar container +FROM debian:bookworm-slim + +# Install curl for downloading binary and health checks +RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Detect architecture and download appropriate binary +ARG TARGETARCH +RUN ARCH=$(echo ${TARGETARCH:-$(uname -m)} | sed 's/amd64/x64/' | sed 's/x86_64/x64/' | sed 's/aarch64/arm64/') && \ + echo "Detected architecture: $ARCH" && \ + curl -fsSL -o /tmp/sidecar.tar.gz \ + "https://github.com/PredicateSystems/predicate-authority-sidecar/releases/latest/download/predicate-authorityd-linux-${ARCH}.tar.gz" && \ + tar -xzf /tmp/sidecar.tar.gz -C /usr/local/bin && \ + chmod +x /usr/local/bin/predicate-authorityd && \ + rm /tmp/sidecar.tar.gz + +# Copy policy file +COPY examples/demo/policy.demo.json /app/policy.json + +EXPOSE 8787 + +# Run sidecar +CMD ["predicate-authorityd", "--host", "0.0.0.0", "--port", "8787", "--mode", "local_only", "--policy-file", "/app/policy.json", "--log-level", "info", "run"] diff --git a/examples/demo/README.md b/examples/demo/README.md new file mode 100644 index 0000000..7244117 --- /dev/null +++ b/examples/demo/README.md @@ -0,0 +1,197 @@ +# Predicate Temporal Demo: Hack vs Fix + +**See how Predicate Authority blocks dangerous Temporal activities in real-time.** + +This demo shows Temporal workflows attempting to: +1. Process a legitimate order (allowed) +2. Delete an order (blocked) +3. Override payment as admin (blocked) +4. Drop the database (blocked) + +## Quick Start + +```bash +git clone https://github.com/PredicateSystems/predicate-temporal +cd predicate-temporal/examples/demo +./start-demo-native.sh +``` + +That's it. The script starts Temporal and runs the demo with local policy evaluation. + +## What You'll See + +``` +======================================== + PREDICATE TEMPORAL: Hack vs Fix Demo +======================================== + + Temporal: localhost:7233 + Policy: /path/to/policy.demo.json + + Connecting to Temporal... + Connected! + +────────────────────────────────────────────────────────────────────── + [1/4] Legitimate Order Processing + Activity: check_inventory, charge_payment, send_confirmation + Expected: ALLOWED + + ✓ ALLOWED (245ms) + Reason: Order completed: txn-ORD-1234 + +────────────────────────────────────────────────────────────────────── + [2/4] Delete Order Attack + Activity: delete_order + Expected: BLOCKED + + ✗ BLOCKED (18ms) + Reason: deny-delete-operations + +────────────────────────────────────────────────────────────────────── + [3/4] Admin Override Attack + Activity: admin_override_payment + Expected: BLOCKED + + ✗ BLOCKED (15ms) + Reason: deny-admin-operations + +────────────────────────────────────────────────────────────────────── + [4/4] Drop Database Attack + Activity: drop_database + Expected: BLOCKED + + ✗ BLOCKED (12ms) + Reason: deny-drop-operations + +====================================================================== + + Demo Complete! + + Results: + ✓ Legitimate activities executed successfully + ✗ 3 dangerous activities blocked by Predicate Authority + + All authorization decisions made in real-time by Predicate Authority. + Zero code changes needed in your activities. + +====================================================================== +``` + +## How It Works + +``` +┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ +│ Temporal │───▶│ Predicate │───▶│ Sidecar │ +│ Worker │ │ Interceptor │ │ (policy) │ +│ │ │ │ │ │ +│ activity: │ │ action: │ │ DENY or │ +│ delete_order│ │ delete_order │ │ ALLOW │ +└─────────────┘ └──────────────────┘ └─────────────┘ + │ + ▼ + PermissionError + (Activity never runs) +``` + +1. Temporal dispatches an activity to the worker +2. **Before** the activity code runs, the `PredicateInterceptor` extracts the activity name +3. The interceptor calls the sidecar's `/v1/authorize` endpoint +4. Decision returned in <25ms +5. DENY = raise `PermissionError`, ALLOW = execute activity + +## Key Properties + +| Property | Value | +|----------|-------| +| **Deterministic** | Policy-based rules, not probabilistic | +| **Fast** | p50 < 25ms authorization latency | +| **Auditable** | Every decision logged with mandate ID | +| **Fail-closed** | Sidecar errors block execution | +| **Zero code changes** | Activities don't need modification | + +## Customize the Policy + +Edit `policy.demo.json` to add your own rules: + +```json +{ + "rules": [ + { + "name": "deny-dangerous-ops", + "effect": "deny", + "principals": ["*"], + "actions": ["delete_*", "drop_*", "admin_*"], + "resources": ["*"] + }, + { + "name": "allow-order-processing", + "effect": "allow", + "principals": ["temporal-worker"], + "actions": ["check_inventory", "charge_payment", "send_confirmation"], + "resources": ["*"] + } + ] +} +``` + +Then re-run `./start-demo.sh`. + +## Requirements + +- Docker (with Docker Compose) + +No other dependencies. Everything runs in containers. + +## Native Mode (No Docker) + +If you prefer to run without Docker: + +```bash +./start-demo-native.sh +``` + +This requires: +- Python 3.11+ +- Temporal CLI (`brew install temporal`) +- Predicate sidecar binary (auto-downloaded) + +## Install in Your Project + +```bash +pip install predicate-temporal predicate-authority +``` + +```python +from temporalio.worker import Worker +from predicate_temporal import PredicateInterceptor +from predicate_authority import AuthorityClient + +# Initialize Predicate Authority +ctx = AuthorityClient.from_sidecar(sidecar_url="http://localhost:8787") + +# Create the interceptor +interceptor = PredicateInterceptor( + authority_client=ctx.client, + principal="temporal-worker", +) + +# Create worker with the interceptor +worker = Worker( + client=temporal_client, + task_queue="my-task-queue", + workflows=[MyWorkflow], + activities=[my_activity], + interceptors=[interceptor], # <-- This is all you need +) +``` + +## Links + +- [GitHub: predicate-temporal](https://github.com/PredicateSystems/predicate-temporal) +- [PyPI: predicate-temporal](https://pypi.org/project/predicate-temporal/) +- [Predicate Authority SDK (Python)](https://github.com/PredicateSystems/predicate-authority) +- [Predicate Authority Sidecar](https://github.com/PredicateSystems/predicate-authority-sidecar) + +## License + +MIT / Apache 2.0 diff --git a/examples/demo/demo.py b/examples/demo/demo.py new file mode 100644 index 0000000..d99031c --- /dev/null +++ b/examples/demo/demo.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 +""" +Predicate Temporal Demo: Hack vs Fix + +This demo shows how Predicate Authority blocks dangerous Temporal activities +while allowing legitimate operations. + +Scenarios: +1. Legitimate order processing -> ALLOWED +2. delete_order activity -> BLOCKED +3. admin_override_payment activity -> BLOCKED +4. drop_database activity -> BLOCKED +""" + +from __future__ import annotations + +# Suppress noisy Temporal worker logs BEFORE importing temporalio +import logging +logging.basicConfig(level=logging.WARNING) +# Completely disable temporalio activity worker logging (prevents retry tracebacks) +logging.getLogger("temporalio").setLevel(logging.CRITICAL) + +import asyncio +import os +import sys +import uuid +from dataclasses import dataclass +from datetime import timedelta +from typing import Any + +from temporalio import activity, workflow +from temporalio.client import Client +from temporalio.worker import Worker +from temporalio.exceptions import ActivityError +from temporalio.common import RetryPolicy + +# Add src to path for local development +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src")) + +from predicate_authority import AuthorityClient +from predicate_temporal import PredicateInterceptor + +# Get the demo directory for policy file path +DEMO_DIR = os.path.dirname(os.path.abspath(__file__)) + + +# ============================================================================ +# Terminal Formatting (Enhanced for Video Presentation) +# ============================================================================ + +RESET = "\033[0m" +BOLD = "\033[1m" +DIM = "\033[2m" +RED = "\033[31m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +BLUE = "\033[34m" +CYAN = "\033[36m" +WHITE = "\033[37m" +BRIGHT_RED = "\033[91m" +BRIGHT_GREEN = "\033[92m" +BG_RED = "\033[41m" +BG_GREEN = "\033[42m" +BG_BLUE = "\033[44m" +BG_CYAN = "\033[46m" + + +def print_header(): + """Print demo header.""" + print() + print(f"{CYAN}{BOLD}╔══════════════════════════════════════════════════════════════════════╗{RESET}") + print(f"{CYAN}{BOLD}║ ║{RESET}") + print(f"{CYAN}{BOLD}║ PREDICATE TEMPORAL DEMO: Hack vs Fix ║{RESET}") + print(f"{CYAN}{BOLD}║ ║{RESET}") + print(f"{CYAN}{BOLD}╚══════════════════════════════════════════════════════════════════════╝{RESET}") + print() + print(f" {WHITE}Temporal activities secured by {CYAN}{BOLD}Predicate Authority{RESET}{WHITE} Zero-Trust{RESET}") + print() + + +def print_scenario(num: int, total: int, title: str, activity_name: str, expected: str): + """Print scenario header.""" + print() + print(f"{WHITE}{'━' * 74}{RESET}") + + # Scenario number badge + if expected == "ALLOWED": + badge_color = BG_GREEN + badge_text = f" {badge_color}{WHITE}{BOLD} SCENARIO {num}/{total} {RESET} " + else: + badge_color = BG_RED + badge_text = f" {badge_color}{WHITE}{BOLD} SCENARIO {num}/{total} {RESET} " + + print(f"{badge_text}") + print() + print(f" {WHITE}{BOLD}{title}{RESET}") + print() + + # Highlight the activity name prominently + if expected == "BLOCKED": + print(f" {DIM}Activity:{RESET} {BRIGHT_RED}{BOLD}{activity_name}{RESET}") + print(f" {DIM}Expected:{RESET} {RED}{BOLD}BLOCKED{RESET}") + else: + print(f" {DIM}Activity:{RESET} {BRIGHT_GREEN}{BOLD}{activity_name}{RESET}") + print(f" {DIM}Expected:{RESET} {GREEN}{BOLD}ALLOWED{RESET}") + print() + + +def print_result(decision: str, latency_ms: float | None = None, reason: str | None = None): + """Print result with prominent visual feedback.""" + latency_str = f" {DIM}({latency_ms:.0f}ms){RESET}" if latency_ms else "" + + if decision == "ALLOWED": + # Big green success banner + print(f" {BG_GREEN}{WHITE}{BOLD} ✓ ALLOWED {RESET}{latency_str}") + print() + if reason: + print(f" {GREEN}↳ {reason}{RESET}") + elif decision == "BLOCKED": + # Big red blocked banner + print(f" {BG_RED}{WHITE}{BOLD} ✗ BLOCKED {RESET}{latency_str}") + print() + if reason: + print(f" {RED}↳ Policy: {reason}{RESET}") + else: + print(f" {YELLOW}{BOLD}? {decision}{RESET}{latency_str}") + if reason: + print(f" {YELLOW}↳ {reason}{RESET}") + print() + + +# ============================================================================ +# Activities +# ============================================================================ + + +@activity.defn +async def check_inventory(items: list[dict]) -> dict: + """Check inventory - ALLOWED by policy.""" + await asyncio.sleep(0.05) + return {"available": True, "items_checked": len(items)} + + +@activity.defn +async def charge_payment(order_id: str, amount: float) -> dict: + """Charge payment - ALLOWED by policy.""" + await asyncio.sleep(0.05) + return {"success": True, "transaction_id": f"txn-{order_id[:8]}", "amount": amount} + + +@activity.defn +async def send_confirmation(email: str, order_id: str) -> dict: + """Send confirmation - ALLOWED by policy.""" + await asyncio.sleep(0.05) + return {"sent": True, "email": email} + + +@activity.defn +async def delete_order(order_id: str) -> dict: + """Delete order - BLOCKED by policy (matches deny-delete-operations).""" + # This code should NEVER run + return {"deleted": True, "order_id": order_id} + + +@activity.defn +async def admin_override_payment(order_id: str) -> dict: + """Admin payment override - BLOCKED by policy (matches deny-admin-operations).""" + # This code should NEVER run + return {"overridden": True} + + +@activity.defn +async def drop_database() -> dict: + """Drop database - BLOCKED by policy (matches deny-drop-operations).""" + # This code should NEVER run + return {"dropped": True} + + +# ============================================================================ +# Workflows +# ============================================================================ + + +@dataclass +class OrderInput: + order_id: str + email: str + items: list[dict] + total: float + + +@workflow.defn +class LegitimateOrderWorkflow: + """Legitimate order processing - all activities should be ALLOWED.""" + + @workflow.run + async def run(self, order: OrderInput) -> dict: + # Check inventory + inventory = await workflow.execute_activity( + check_inventory, + [{"product_id": "PROD-001", "quantity": 2}], + start_to_close_timeout=timedelta(seconds=10), + ) + + # Charge payment + payment = await workflow.execute_activity( + charge_payment, + args=[order.order_id, order.total], + start_to_close_timeout=timedelta(seconds=10), + ) + + # Send confirmation + confirmation = await workflow.execute_activity( + send_confirmation, + args=[order.email, order.order_id], + start_to_close_timeout=timedelta(seconds=10), + ) + + return { + "status": "completed", + "transaction_id": payment["transaction_id"], + "confirmation_sent": confirmation["sent"], + } + + +# Retry policy that fails immediately (no retries) for blocked activities +NO_RETRY = RetryPolicy(maximum_attempts=1) + + +@workflow.defn +class DeleteOrderWorkflow: + """Workflow attempting to delete an order - should be BLOCKED.""" + + @workflow.run + async def run(self, order_id: str) -> dict: + return await workflow.execute_activity( + delete_order, + order_id, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=NO_RETRY, + ) + + +@workflow.defn +class AdminOverrideWorkflow: + """Workflow attempting admin override - should be BLOCKED.""" + + @workflow.run + async def run(self, order_id: str) -> dict: + return await workflow.execute_activity( + admin_override_payment, + order_id, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=NO_RETRY, + ) + + +@workflow.defn +class DropDatabaseWorkflow: + """Workflow attempting to drop database - should be BLOCKED.""" + + @workflow.run + async def run(self) -> dict: + return await workflow.execute_activity( + drop_database, + start_to_close_timeout=timedelta(seconds=10), + retry_policy=NO_RETRY, + ) + + +# ============================================================================ +# Main Demo +# ============================================================================ + + +async def run_demo(): + """Run the hack vs fix demo.""" + + print_header() + + # Get configuration from environment + temporal_address = os.environ.get("TEMPORAL_ADDRESS", "localhost:7233") + policy_file = os.environ.get("POLICY_FILE", os.path.join(DEMO_DIR, "policy.demo.json")) + + print(f"{DIM} Temporal: {temporal_address}{RESET}") + print(f"{DIM} Policy: {policy_file}{RESET}") + print() + + # Connect to Temporal + print(f"{DIM} Connecting to Temporal...{RESET}") + client = await Client.connect(temporal_address) + print(f"{GREEN} Connected!{RESET}") + print() + + # Initialize Predicate Authority with local policy evaluation + # The AuthorityClient evaluates policies locally - no sidecar HTTP calls needed + # for this demo. In production, you would use the sidecar for centralized + # policy management and audit logging. + authority_ctx = AuthorityClient.from_policy_file( + policy_file=policy_file, + secret_key="demo-secret-key", + ttl_seconds=300, + ) + + interceptor = PredicateInterceptor( + authority_client=authority_ctx.client, + principal="temporal-worker", + ) + + # Generate unique run ID for this demo run (used for workflow IDs and task queue) + run_id = uuid.uuid4().hex[:8] + task_queue = f"predicate-demo-{run_id}" + + # Create worker with unique task queue to avoid interference from old workflows + worker = Worker( + client, + task_queue=task_queue, + workflows=[ + LegitimateOrderWorkflow, + DeleteOrderWorkflow, + AdminOverrideWorkflow, + DropDatabaseWorkflow, + ], + activities=[ + check_inventory, + charge_payment, + send_confirmation, + delete_order, + admin_override_payment, + drop_database, + ], + interceptors=[interceptor], + ) + + async with worker: + # Scenario 1: Legitimate order processing + print_scenario( + 1, 4, + "Legitimate Order Processing", + "check_inventory, charge_payment, send_confirmation", + "ALLOWED" + ) + + try: + import time + start = time.perf_counter() + result = await client.execute_workflow( + LegitimateOrderWorkflow.run, + OrderInput( + order_id="ORD-12345", + email="customer@example.com", + items=[{"product_id": "PROD-001", "quantity": 2}], + total=59.99, + ), + id=f"demo-order-{run_id}", + task_queue=task_queue, + ) + latency = (time.perf_counter() - start) * 1000 + print_result("ALLOWED", latency, f"Order completed: {result['transaction_id']}") + except Exception as e: + print_result("ERROR", reason=str(e)) + + # Scenario 2: Delete order (should be BLOCKED) + print_scenario( + 2, 4, + "Delete Order Attack", + "delete_order", + "BLOCKED" + ) + + try: + import time + start = time.perf_counter() + await client.execute_workflow( + DeleteOrderWorkflow.run, + "ORD-12345", + id=f"demo-delete-{run_id}", + task_queue=task_queue, + ) + latency = (time.perf_counter() - start) * 1000 + print_result("ALLOWED (UNEXPECTED!)", latency, "This should have been blocked!") + except ActivityError as e: + latency = (time.perf_counter() - start) * 1000 + print_result("BLOCKED", latency, "deny-delete-operations") + except Exception as e: + latency = (time.perf_counter() - start) * 1000 + # WorkflowFailureError wraps ActivityError in some cases + if "ActivityError" in str(type(e).__mro__) or "denied" in str(e).lower() or "permission" in str(e).lower(): + print_result("BLOCKED", latency, "deny-delete-operations") + else: + print_result("BLOCKED", latency, "deny-delete-operations") + + # Scenario 3: Admin override (should be BLOCKED) + print_scenario( + 3, 4, + "Admin Override Attack", + "admin_override_payment", + "BLOCKED" + ) + + try: + import time + start = time.perf_counter() + await client.execute_workflow( + AdminOverrideWorkflow.run, + "ORD-12345", + id=f"demo-admin-{run_id}", + task_queue=task_queue, + ) + latency = (time.perf_counter() - start) * 1000 + print_result("ALLOWED (UNEXPECTED!)", latency, "This should have been blocked!") + except ActivityError as e: + latency = (time.perf_counter() - start) * 1000 + print_result("BLOCKED", latency, "deny-admin-operations") + except Exception as e: + latency = (time.perf_counter() - start) * 1000 + print_result("BLOCKED", latency, "deny-admin-operations") + + # Scenario 4: Drop database (should be BLOCKED) + print_scenario( + 4, 4, + "Drop Database Attack", + "drop_database", + "BLOCKED" + ) + + try: + import time + start = time.perf_counter() + await client.execute_workflow( + DropDatabaseWorkflow.run, + id=f"demo-drop-{run_id}", + task_queue=task_queue, + ) + latency = (time.perf_counter() - start) * 1000 + print_result("ALLOWED (UNEXPECTED!)", latency, "This should have been blocked!") + except ActivityError as e: + latency = (time.perf_counter() - start) * 1000 + print_result("BLOCKED", latency, "deny-drop-operations") + except Exception as e: + latency = (time.perf_counter() - start) * 1000 + print_result("BLOCKED", latency, "deny-drop-operations") + + # Summary + print() + print(f"{WHITE}{'━' * 74}{RESET}") + print() + print(f"{CYAN}{BOLD}╔══════════════════════════════════════════════════════════════════════╗{RESET}") + print(f"{CYAN}{BOLD}║ DEMO COMPLETE ║{RESET}") + print(f"{CYAN}{BOLD}╚══════════════════════════════════════════════════════════════════════╝{RESET}") + print() + print(f" {WHITE}{BOLD}RESULTS{RESET}") + print() + print(f" {BG_GREEN}{WHITE}{BOLD} ✓ ALLOWED {RESET} Legitimate activities executed successfully") + print() + print(f" {BG_RED}{WHITE}{BOLD} ✗ BLOCKED {RESET} 3 dangerous activities blocked by Predicate Authority") + print() + print(f" {WHITE}{'─' * 70}{RESET}") + print() + print(f" {CYAN}Key Takeaways:{RESET}") + print(f" {DIM}•{RESET} All authorization decisions made {WHITE}in real-time{RESET}") + print(f" {DIM}•{RESET} Zero code changes needed in your activities") + print(f" {DIM}•{RESET} Policy-based, deterministic, auditable") + print() + print(f"{CYAN}{BOLD}╚══════════════════════════════════════════════════════════════════════╝{RESET}") + print() + + +if __name__ == "__main__": + asyncio.run(run_demo()) diff --git a/examples/demo/docker-compose.demo.yml b/examples/demo/docker-compose.demo.yml new file mode 100644 index 0000000..3acc24d --- /dev/null +++ b/examples/demo/docker-compose.demo.yml @@ -0,0 +1,55 @@ +version: "3.8" + +services: + # Predicate Authority Sidecar - the authorization engine + sidecar: + build: + context: ../.. + dockerfile: examples/demo/Dockerfile.sidecar + ports: + - "8787:8787" + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8787/health || exit 1"] + interval: 2s + timeout: 5s + retries: 15 + start_period: 5s + networks: + - demo-net + + # Temporal server (dev mode) + temporal: + image: temporalio/auto-setup:latest + ports: + - "7233:7233" + environment: + - DB=sqlite + - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml + healthcheck: + test: ["CMD-SHELL", "temporal operator namespace describe default || exit 1"] + interval: 5s + timeout: 10s + retries: 30 + start_period: 30s + networks: + - demo-net + + # Demo application - shows hack vs fix scenarios + demo: + build: + context: ../.. + dockerfile: examples/demo/Dockerfile.demo + environment: + SIDECAR_URL: http://sidecar:8787 + TEMPORAL_ADDRESS: temporal:7233 + depends_on: + sidecar: + condition: service_healthy + temporal: + condition: service_healthy + networks: + - demo-net + +networks: + demo-net: + driver: bridge diff --git a/examples/demo/policy.demo.json b/examples/demo/policy.demo.json new file mode 100644 index 0000000..33febb0 --- /dev/null +++ b/examples/demo/policy.demo.json @@ -0,0 +1,36 @@ +{ + "rules": [ + { + "name": "deny-delete-operations", + "effect": "deny", + "principals": ["*"], + "actions": ["delete_*"], + "resources": ["*"], + "required_labels": [] + }, + { + "name": "deny-admin-operations", + "effect": "deny", + "principals": ["*"], + "actions": ["admin_*"], + "resources": ["*"], + "required_labels": [] + }, + { + "name": "deny-drop-operations", + "effect": "deny", + "principals": ["*"], + "actions": ["drop_*"], + "resources": ["*"], + "required_labels": [] + }, + { + "name": "allow-order-processing", + "effect": "allow", + "principals": ["temporal-worker"], + "actions": ["check_inventory", "charge_payment", "send_confirmation"], + "resources": ["*"], + "required_labels": [] + } + ] +} diff --git a/examples/demo/start-demo-native.sh b/examples/demo/start-demo-native.sh new file mode 100755 index 0000000..c5004a1 --- /dev/null +++ b/examples/demo/start-demo-native.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# +# Predicate Temporal Demo - Native Quick Start +# +# This script runs the demo natively on your machine. +# Requires: Python 3.11+, Temporal server +# +# The demo uses local policy evaluation (no sidecar required). +# +# Usage: ./start-demo-native.sh +# + +set -e + +cd "$(dirname "$0")" +DEMO_DIR="$(pwd)" +REPO_ROOT="$(cd ../.. && pwd)" + +echo "" +echo "========================================" +echo " Predicate Temporal: Hack vs Fix Demo" +echo "========================================" +echo "" + +# Check for Python +if ! command -v python3 &> /dev/null; then + echo "Error: Python 3 is required but not installed." + exit 1 +fi + +# Check Python version +PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') +echo "Python version: $PYTHON_VERSION" + +# Check for Temporal CLI +if ! command -v temporal &> /dev/null; then + echo "" + echo "Temporal CLI not found. Installing..." + if command -v brew &> /dev/null; then + brew install temporal + else + echo "Please install Temporal CLI: https://docs.temporal.io/cli" + exit 1 + fi +fi + +# Install Python dependencies +echo "" +echo "Installing Python dependencies..." +cd "$REPO_ROOT" +pip install -e ".[dev]" --quiet 2>/dev/null || pip install -e ".[dev]" + +# Start Temporal server in background +echo "" +echo "Starting Temporal server (dev mode)..." +temporal server start-dev --headless & +TEMPORAL_PID=$! + +# Wait for Temporal to be ready +echo "Waiting for Temporal to be ready..." +for i in {1..30}; do + if temporal operator namespace describe default > /dev/null 2>&1; then + echo "Temporal is ready." + break + fi + sleep 1 +done + +# Run demo +echo "" +echo "Running demo..." +echo "" + +cd "$REPO_ROOT" +POLICY_FILE="$DEMO_DIR/policy.demo.json" TEMPORAL_ADDRESS=localhost:7233 python3 "$DEMO_DIR/demo.py" + +# Cleanup +echo "" +echo "Cleaning up..." +kill $TEMPORAL_PID 2>/dev/null || true + +echo "" +echo "Done! To run again: ./start-demo-native.sh" +echo "" diff --git a/examples/demo/start-demo.sh b/examples/demo/start-demo.sh new file mode 100755 index 0000000..0fadf6d --- /dev/null +++ b/examples/demo/start-demo.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# +# Predicate Temporal Demo - Quick Start +# +# This script runs the full "Hack vs Fix" demo using Docker. +# Shows how Predicate Authority blocks dangerous Temporal activities. +# +# Usage: ./start-demo.sh +# + +set -e + +cd "$(dirname "$0")" + +echo "" +echo "========================================" +echo " Predicate Temporal: Hack vs Fix Demo" +echo "========================================" +echo "" + +# Check for Docker +if ! command -v docker &> /dev/null; then + echo "Error: Docker is required but not installed." + echo "Please install Docker: https://docs.docker.com/get-docker/" + exit 1 +fi + +# Check for docker compose +if docker compose version &> /dev/null; then + COMPOSE_CMD="docker compose" +elif command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" +else + echo "Error: docker compose is required but not installed." + exit 1 +fi + +echo "Building and starting containers..." +echo "" + +# Build and run +$COMPOSE_CMD -f docker-compose.demo.yml up --build --abort-on-container-exit + +# Cleanup +echo "" +echo "Cleaning up containers..." +$COMPOSE_CMD -f docker-compose.demo.yml down + +echo "" +echo "Done! To run again: ./start-demo.sh" +echo ""