Skip to content

Commit 8e64ec7

Browse files
Add custom decision functions with visibility-level evaluation
Introduce decision functions as standalone WASM-based policy gates that control whether a policy fires. Key components: - Decision function CRUD API with Javy CLI compilation (JS → WASM) - Query-time evaluation in PolicyHook via wasmtime (dynamic two-module linking) - Visibility-level evaluation in EngineCache for column_deny/table_deny/column_allow - evaluate_context="session" evaluates at both visibility and query time - evaluate_context="query" skips visibility (deferred to query time) - Robust stdout parsing (parse_stdout_result) handles Javy 8.x console.log routing to stdout without requiring WASM recompilation - 4 migrations: decision_function table, name index, policy FK, wasm clear - Admin UI integration: PolicyForm decision function selector, audit display - 15 integration tests, 16 unit tests covering all DF scenarios
1 parent 5b57e0f commit 8e64ec7

39 files changed

Lines changed: 6809 additions & 193 deletions

Cargo.lock

Lines changed: 921 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Dockerfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
2020
pkg-config \
2121
&& rm -rf /var/lib/apt/lists/*
2222

23-
# Copy workspace manifests and lockfile for dependency caching
23+
# Copy workspace manifests, lockfile, and build script for dependency caching
2424
COPY Cargo.toml Cargo.lock ./
2525
COPY proxy/Cargo.toml proxy/Cargo.toml
26+
COPY proxy/build.rs proxy/build.rs
2627
COPY migration/ migration/
2728

2829
# Create a dummy binary so `cargo build` can cache dependencies
30+
# build.rs downloads Javy CLI and emits plugin.wasm (cached in OUT_DIR)
2931
RUN mkdir -p proxy/src && echo 'fn main() {}' > proxy/src/main.rs \
3032
&& cargo build --release \
3133
&& rm -rf proxy/src
@@ -47,7 +49,9 @@ FROM base AS builder
4749

4850
COPY proxy/src proxy/src
4951

50-
RUN cargo build --release
52+
RUN cargo build --release \
53+
&& cp target/release/javy /usr/local/bin/javy \
54+
&& cp target/release/engine.wasm /usr/local/lib/engine.wasm
5155

5256
# ── prod: minimal runtime image ───────────────────────────────────────────────
5357
FROM debian:bookworm-slim AS prod
@@ -61,6 +65,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
6165
RUN mkdir -p /data && chown -R 1000:1000 /data
6266

6367
COPY --from=builder /app/target/release/proxy /usr/local/bin/proxy
68+
COPY --from=builder /usr/local/bin/javy /usr/local/bin/javy
69+
COPY --from=builder /usr/local/lib/engine.wasm /usr/local/lib/engine.wasm
6470
COPY --from=ui-builder /app/admin-ui/dist /usr/local/share/admin-ui
6571

6672
ENV BR_PROXY_BIND_ADDR=0.0.0.0:5434

admin-ui/CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ React 19, Vite 6, Tailwind 4, TanStack Query 5, react-router-dom 7, Vitest 3, @t
2323
- `src/pages/AdminAuditPage.tsx` — Centralized admin audit log with filters (resource type, actor, date range)
2424
- `src/types/policy.ts` — TypeScript interfaces for policies, assignments (`PolicyType`, `AssignmentScope`, `TargetEntry`)
2525
- `src/types/role.ts` — TypeScript interfaces for roles, members, audit entries
26+
- `src/types/decisionFunction.ts` — TypeScript interfaces for decision functions (`DecisionFunction`, `DecisionFunctionCreate`, `DecisionFunctionUpdate`)
27+
- `src/api/decisionFunctions.ts` — API client functions for decision function CRUD (`listDecisionFunctions`, `getDecisionFunction`, `createDecisionFunction`, `updateDecisionFunction`, `deleteDecisionFunction`)
2628
- `src/test/test-utils.tsx``renderWithProviders` (QueryClient + AuthProvider + MemoryRouter)
2729
- `src/test/factories.ts``makeUser`, `makeDataSource`, `makeDataSourceType`, `makeDiscoveredSchema/Table/Column`
2830

admin-ui/src/api/audit.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,18 @@ export interface AuditLogEntry {
99
datasource_name: string
1010
original_query: string
1111
rewritten_query: string | null
12-
policies_applied: Array<{ policy_id: string; version: number; name: string }>
12+
policies_applied: Array<{
13+
policy_id: string
14+
version: number
15+
name: string
16+
decision?: {
17+
result?: { fire: boolean; fuel_consumed?: number; time_us?: number }
18+
logs?: string[]
19+
fuel_consumed?: number
20+
time_us?: number
21+
error?: string | null
22+
}
23+
}>
1324
execution_time_ms: number | null
1425
client_ip: string | null
1526
client_info: string | null
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { client } from './client'
2+
import type {
3+
DecisionFunctionResponse,
4+
CreateDecisionFunctionPayload,
5+
UpdateDecisionFunctionPayload,
6+
TestDecisionFnPayload,
7+
TestDecisionFnResponse,
8+
} from '../types/decisionFunction'
9+
10+
export async function createDecisionFunction(
11+
payload: CreateDecisionFunctionPayload,
12+
): Promise<DecisionFunctionResponse> {
13+
const { data } = await client.post<DecisionFunctionResponse>('/decision-functions', payload)
14+
return data
15+
}
16+
17+
export async function getDecisionFunction(id: string): Promise<DecisionFunctionResponse> {
18+
const { data } = await client.get<DecisionFunctionResponse>(`/decision-functions/${id}`)
19+
return data
20+
}
21+
22+
export async function updateDecisionFunction(
23+
id: string,
24+
payload: UpdateDecisionFunctionPayload,
25+
): Promise<DecisionFunctionResponse> {
26+
const { data } = await client.put<DecisionFunctionResponse>(`/decision-functions/${id}`, payload)
27+
return data
28+
}
29+
30+
export async function deleteDecisionFunction(id: string): Promise<void> {
31+
await client.delete(`/decision-functions/${id}`)
32+
}
33+
34+
export async function testDecisionFn(
35+
payload: TestDecisionFnPayload,
36+
): Promise<TestDecisionFnResponse> {
37+
const { data } = await client.post<TestDecisionFnResponse>('/decision-functions/test', payload)
38+
return data
39+
}

0 commit comments

Comments
 (0)