Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
52 changes: 52 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: webui/package-lock.json

- name: Build Web UI
run: |
cd webui
npm ci
npm run build

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
Expand Down Expand Up @@ -50,6 +63,19 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: webui/package-lock.json

- name: Build Web UI
run: |
cd webui
npm ci
npm run build

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
Expand Down Expand Up @@ -80,6 +106,19 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: webui/package-lock.json

- name: Build Web UI
run: |
cd webui
npm ci
npm run build

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
Expand Down Expand Up @@ -115,6 +154,19 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: webui/package-lock.json

- name: Build Web UI
run: |
cd webui
npm ci
npm run build

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: webui/package-lock.json

- name: Build Web UI
run: |
cd webui
npm ci
npm run build

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
Expand Down
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ anyhow = "1"
parking_lot = "0.12"
urlencoding = "2"

# Web UI (embedded static files)
rust-embed = "8"
mime_guess = "2"
futures = "0.3"
async-stream = "0.3"
tokio-stream = "0.1"

[dev-dependencies]
tokio-test = "0.4"
criterion = { version = "0.5", features = ["html_reports"] }
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,40 @@ Watch authorization decisions in real-time with the built-in TUI:

---

## Web UI

Browser-based monitoring dashboard with real-time authorization event streaming:

![Web UI Screenshot](docs/images/web-ui.png)

```bash
./predicate-authorityd --policy-file policy.json --web-ui run
```

On startup, a secure URL is printed to the terminal:

```
Web UI enabled: http://127.0.0.1:8787/ui/?token=a1b2c3d4e5f6...
```

**Features:**
- **Split-pane layout:** Policy viewer on the left, live event feed on the right
- **Real-time streaming:** Events appear instantly via Server-Sent Events (SSE)
- **Color-coded results:** Green for ALLOW, red for DENY
- **Event filtering:** Filter by principal, action, or result type
- **Statistics:** Total allowed/denied counts and average latency
- **Copy URL button:** Share the dashboard URL (includes auth token)
- **Connection status:** Visual indicator when SSE connection is active

**Security:**
- Token-based authentication (32-character random token)
- Token stored in `sessionStorage` (cleared on tab close)
- URL cleaned after token extraction (no token in history)

The `--web-ui` flag works with both `run` and `dashboard` commands. When used with `dashboard`, both the TUI and Web UI run simultaneously.

---

## Quick Start

**30 seconds to your first authorization decision.**
Expand Down
136 changes: 136 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
#
# Build script for predicate-authorityd with embedded Web UI
#
# Usage:
# ./build.sh # Build release binary
# ./build.sh --debug # Build debug binary
# ./build.sh --clean # Clean and rebuild everything
#

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}

log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}

log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}

# Parse arguments
BUILD_MODE="release"
CLEAN=false

for arg in "$@"; do
case $arg in
--debug)
BUILD_MODE="debug"
;;
--clean)
CLEAN=true
;;
--help|-h)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --debug Build debug binary instead of release"
echo " --clean Clean and rebuild everything"
echo " --help Show this help message"
exit 0
;;
*)
log_error "Unknown option: $arg"
exit 1
;;
esac
done

# Clean if requested
if [ "$CLEAN" = true ]; then
log_info "Cleaning previous builds..."
rm -rf webui/dist webui/node_modules target
fi

# Check for required tools
log_info "Checking prerequisites..."

if ! command -v node &> /dev/null; then
log_error "Node.js is not installed. Please install Node.js 18+ first."
exit 1
fi

if ! command -v npm &> /dev/null; then
log_error "npm is not installed. Please install npm first."
exit 1
fi

if ! command -v cargo &> /dev/null; then
log_error "Rust/Cargo is not installed. Please install Rust first."
exit 1
fi

NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
log_warn "Node.js version $NODE_VERSION detected. Recommended: 18+"
fi

# Step 1: Build Web UI
log_info "Building Web UI (React + Vite)..."
cd webui

if [ ! -d "node_modules" ]; then
log_info "Installing npm dependencies..."
npm ci
fi

npm run build

if [ ! -f "dist/index.html" ]; then
log_error "Web UI build failed - dist/index.html not found"
exit 1
fi

log_info "Web UI built successfully ($(du -sh dist | cut -f1))"
cd ..

# Step 2: Touch static_files.rs to force re-embed
# This ensures rust-embed picks up the new dist files
touch src/webui/static_files.rs

# Step 3: Build Rust binary
log_info "Building Rust binary ($BUILD_MODE mode)..."

if [ "$BUILD_MODE" = "release" ]; then
cargo build --release
BINARY_PATH="target/release/predicate-authorityd"
else
cargo build
BINARY_PATH="target/debug/predicate-authorityd"
fi

if [ ! -f "$BINARY_PATH" ]; then
log_error "Rust build failed - binary not found"
exit 1
fi

BINARY_SIZE=$(du -h "$BINARY_PATH" | cut -f1)
log_info "Build complete!"
echo ""
echo "Binary: $BINARY_PATH ($BINARY_SIZE)"
echo ""
echo "Run with Web UI:"
echo " $BINARY_PATH --policy-file policies/strict.json --web-ui run"
Binary file added docs/images/web-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use axum::{
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::watch;
use tower_http::cors::{Any, CorsLayer};
use tracing::{debug, info, warn};

Expand Down Expand Up @@ -45,6 +46,12 @@ pub struct AppState {
pub policy_reload_secret: Option<String>,
/// Whether /policy/reload endpoint is disabled
pub policy_reload_disabled: bool,
/// Web UI authentication token (if web UI is enabled)
pub web_ui_token: Option<String>,
/// Policy file path (for Web UI to read raw policy)
pub policy_file_path: Option<std::path::PathBuf>,
/// Shutdown signal receiver for SSE streams
pub shutdown_rx: Option<watch::Receiver<bool>>,
}

impl AppState {
Expand All @@ -61,6 +68,9 @@ impl AppState {
identity_mode: "local".to_string(),
policy_reload_secret: None,
policy_reload_disabled: false,
web_ui_token: None,
policy_file_path: None,
shutdown_rx: None,
}
}

Expand Down Expand Up @@ -94,6 +104,21 @@ impl AppState {
self.mandate_store = Some(Arc::new(mandate_store));
self
}

pub fn with_web_ui_token(mut self, token: Option<String>) -> Self {
self.web_ui_token = token;
self
}

pub fn with_policy_file_path(mut self, path: Option<std::path::PathBuf>) -> Self {
self.policy_file_path = path;
self
}

pub fn with_shutdown_signal(mut self, rx: watch::Receiver<bool>) -> Self {
self.shutdown_rx = Some(rx);
self
}
}

/// Create the HTTP router with all endpoints
Expand Down Expand Up @@ -126,6 +151,12 @@ pub fn create_router(state: AppState) -> Router {
router = router.route("/policy/reload", post(policy_reload_handler));
}

// Add Web UI routes if token is configured
if state.web_ui_token.is_some() {
let webui_router = crate::webui::create_webui_router(state.clone());
router = router.merge(webui_router);
}

router
// Operations
.route("/health", get(health_handler))
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ pub mod proof;
pub mod secrets;
pub mod ssrf;
pub mod ui;
pub mod webui;
Loading
Loading