-
Notifications
You must be signed in to change notification settings - Fork 1
[codex] harden env broker and adaptive log RLS #139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,4 @@ | ||
| export { GET, OPTIONS, dynamic, runtime } from "../route"; | ||
| export const dynamic = "force-dynamic"; | ||
| export const runtime = "nodejs"; | ||
|
|
||
| export { GET, OPTIONS } from "../route"; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,28 @@ | ||
| import { buildAllFirstReviewedMemoryFixtures } from "../lib/services/first-reviewed-memory-fixture-builder"; | ||
| import { runManualWorkflowFixtureHarness } from "../lib/services/operator-manual-workflow-fixture-harness"; | ||
|
|
||
| async function main() { | ||
| console.log("TEST-ONLY FIXTURE HARNESS"); | ||
| console.log("NO PRODUCTION WRITES"); | ||
| console.log("NO MODEL CALLS"); | ||
| console.log("NO SEMANTIC RETRIEVAL"); | ||
| export async function runFirstReviewedMemoryFixtureCli() { | ||
| const lines = [ | ||
| "TEST-ONLY FIXTURE HARNESS", | ||
| "NO PRODUCTION WRITES", | ||
| "NO MODEL CALLS", | ||
| "NO SEMANTIC RETRIEVAL", | ||
| ]; | ||
| const results = []; | ||
| for (const fixture of buildAllFirstReviewedMemoryFixtures()) { | ||
| const result = await runManualWorkflowFixtureHarness({ fixture }); | ||
| results.push(result); | ||
| console.log(`${result.ok ? "PASS" : "FAIL"} ${result.scenario} blocked=${result.expectedBlocked} productionSeed=${result.safeSummary.productionSeed} publicPersistence=${result.safeSummary.publicPersistenceEnabled}`); | ||
| lines.push(`${result.ok ? "PASS" : "FAIL"} ${result.scenario} blocked=${result.expectedBlocked} productionSeed=${result.safeSummary.productionSeed} publicPersistence=${result.safeSummary.publicPersistenceEnabled}`); | ||
| } | ||
| if (results.some((r) => !r.ok)) process.exit(1); | ||
| return { ok: results.every((r) => r.ok), output: `${lines.join("\n")}\n` }; | ||
| } | ||
|
|
||
| async function main() { | ||
| const result = await runFirstReviewedMemoryFixtureCli(); | ||
| process.stdout.write(result.output); | ||
| if (!result.ok) process.exit(1); | ||
| } | ||
|
|
||
| if (process.argv[1]?.replace(/\\/g, "/").endsWith("scripts/verify-first-reviewed-memory-fixture.ts")) { | ||
| main().catch(() => { console.error("FAIL fixture harness error (redacted)"); process.exit(1); }); | ||
| } | ||
| main().catch(() => { console.error("FAIL fixture harness error (redacted)"); process.exit(1); }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| -- Add explicit owner-scoped RLS policies for adaptive log tables. | ||
| -- These tables were created with RLS enabled in Phase 4C, but no authenticated | ||
| -- policies were attached, so user-scoped log writes/reads were blocked by | ||
| -- default. Service-role/admin clients continue to bypass RLS as before. | ||
|
|
||
| alter table public.memory_retrieval_logs enable row level security; | ||
| alter table public.memory_retrieval_logs force row level security; | ||
|
|
||
| alter table public.memory_model_call_logs enable row level security; | ||
| alter table public.memory_model_call_logs force row level security; | ||
|
|
||
| do $$ | ||
| begin | ||
| create policy "memory_retrieval_logs_select_own" | ||
| on public.memory_retrieval_logs | ||
| for select | ||
| to authenticated | ||
| using ((select auth.uid()) = user_id); | ||
| exception when duplicate_object then null; | ||
| end $$; | ||
|
|
||
| do $$ | ||
| begin | ||
| create policy "memory_retrieval_logs_insert_own" | ||
| on public.memory_retrieval_logs | ||
| for insert | ||
| to authenticated | ||
| with check ((select auth.uid()) = user_id); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new authenticated insert policy only checks ownership, so any signed-in user can now insert Useful? React with 👍 / 👎. |
||
| exception when duplicate_object then null; | ||
| end $$; | ||
|
|
||
| do $$ | ||
| begin | ||
| create policy "memory_model_call_logs_select_own" | ||
| on public.memory_model_call_logs | ||
| for select | ||
| to authenticated | ||
| using ((select auth.uid()) = user_id); | ||
| exception when duplicate_object then null; | ||
| end $$; | ||
|
|
||
| do $$ | ||
| begin | ||
| create policy "memory_model_call_logs_insert_own" | ||
| on public.memory_model_call_logs | ||
| for insert | ||
| to authenticated | ||
| with check ((select auth.uid()) = user_id); | ||
| exception when duplicate_object then null; | ||
| end $$; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { NextRequest } from "next/server"; | ||
| import { cookies, headers } from "next/headers"; | ||
| import { requireApiUser } from "@/lib/security/api-auth"; | ||
| import { POST as unlockEnvAdmin } from "@/app/api/admin/env/status/route"; | ||
| import { hasEnvAdminCapability, requireEnvAdmin } from "@/lib/services/env-admin-route-guard"; | ||
|
|
||
| vi.mock("next/headers", () => ({ | ||
| cookies: vi.fn(), | ||
| headers: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/security/api-auth", () => ({ | ||
| requireApiUser: vi.fn(), | ||
| })); | ||
|
|
||
| const envKeys = ["PANDORA_INTERNAL_OPERATOR_TOKEN", "PANDORA_INTERNAL_JOB_TOKEN", "PANDORA_ENV_BROKER_ENABLED"] as const; | ||
| const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]])); | ||
|
|
||
| function mockRequestTokens(input: { bearer?: string; explicit?: string; cookie?: string } = {}) { | ||
| vi.mocked(headers).mockResolvedValue({ | ||
| get(name: string) { | ||
| const normalized = name.toLowerCase(); | ||
| if (normalized === "authorization" && input.bearer) return `Bearer ${input.bearer}`; | ||
| if (normalized === "x-pandora-env-admin-token") return input.explicit ?? null; | ||
| return null; | ||
| }, | ||
| } as never); | ||
| vi.mocked(cookies).mockResolvedValue({ | ||
| get(name: string) { | ||
| return name === "pandora_env_admin" && input.cookie ? { value: input.cookie } : undefined; | ||
| }, | ||
| } as never); | ||
| } | ||
|
|
||
| function mockApiUser(appMetadata: Record<string, unknown>) { | ||
| vi.mocked(requireApiUser).mockResolvedValue({ | ||
| response: null, | ||
| user: { id: "user-1", app_metadata: appMetadata }, | ||
| } as never); | ||
| } | ||
|
|
||
| describe("Env Broker admin guard", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| process.env.PANDORA_ENV_BROKER_ENABLED = "true"; | ||
| delete process.env.PANDORA_INTERNAL_OPERATOR_TOKEN; | ||
| delete process.env.PANDORA_INTERNAL_JOB_TOKEN; | ||
| mockRequestTokens(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| for (const key of envKeys) { | ||
| const value = originalEnv[key]; | ||
| if (value === undefined) delete process.env[key]; | ||
| else process.env[key] = value; | ||
| } | ||
| }); | ||
|
|
||
| it("rejects plain Supabase sessions without env-admin app metadata", async () => { | ||
| mockApiUser({}); | ||
|
|
||
| const result = await requireEnvAdmin(); | ||
|
|
||
| expect(result.user).toBeNull(); | ||
| expect(result.response?.status).toBe(403); | ||
| }); | ||
|
|
||
| it("accepts explicit env-admin capabilities from app metadata", async () => { | ||
| mockApiUser({ adminCapabilities: ["env:admin"] }); | ||
|
|
||
| const result = await requireEnvAdmin(); | ||
|
|
||
| expect(result.response).toBeNull(); | ||
| expect(result.user).toEqual({ id: "user-1", authType: "supabase" }); | ||
| }); | ||
|
|
||
| it("accepts the internal operator token without a Supabase session", async () => { | ||
| process.env.PANDORA_INTERNAL_OPERATOR_TOKEN = "0123456789abcdef01234567"; | ||
| mockRequestTokens({ bearer: "0123456789abcdef01234567" }); | ||
|
|
||
| const result = await requireEnvAdmin(); | ||
|
|
||
| expect(requireApiUser).not.toHaveBeenCalled(); | ||
| expect(result.response).toBeNull(); | ||
| expect(result.user).toEqual({ id: "env-operator-token", authType: "operator_token" }); | ||
| }); | ||
|
|
||
| it("does not treat user-editable profile metadata as authorization", () => { | ||
| expect(hasEnvAdminCapability({ app_metadata: {} } as never)).toBe(false); | ||
| expect(hasEnvAdminCapability({ app_metadata: { role: "env_admin" } } as never)).toBe(true); | ||
| expect(hasEnvAdminCapability({ app_metadata: { roles: ["env:broker"] } } as never)).toBe(true); | ||
| }); | ||
|
|
||
| it("scopes the operator unlock cookie to the guarded page and API routes", async () => { | ||
| const token = "0123456789abcdef01234567"; | ||
| process.env.PANDORA_INTERNAL_OPERATOR_TOKEN = token; | ||
| const form = new FormData(); | ||
| form.set("operator_key", token); | ||
|
|
||
| const response = await unlockEnvAdmin(new NextRequest("https://example.test/api/admin/env/status", { method: "POST", body: form })); | ||
| const setCookie = response.headers.get("set-cookie") ?? ""; | ||
|
|
||
| expect(response.status).toBe(303); | ||
| expect(setCookie).toContain("pandora_env_admin="); | ||
| expect(setCookie).toContain("Path=/"); | ||
| expect(setCookie).toContain("HttpOnly"); | ||
| expect(setCookie).toContain("SameSite=strict"); | ||
| expect(setCookie).toContain("Secure"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a successful unlock, this stores the raw operator/job token as a
Path=/cookie, so every subsequent request to unrelated routes such as/dashboardcarriespandora_env_admineven though only/admin/envand/api/admin/env/*need it. BecausegetConfiguredOperatorTokens()accepts internal production tokens, this broadens where a secret is exposed inside the app; use a route-scoped/opaque unlock cookie instead of sending the raw credential site-wide.Useful? React with 👍 / 👎.