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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-- Phase 1 Migration #3: CompanyMission RLS (TENANT_DIRECT)
-- Apply order: #3 of 14 — internal config table; 1:1 with org
-- Runbook: docs/rls-phase-1-cutover-runbook.md §2.3
-- Template: skills/rls-rollout/templates/tenant-direct.sql.template

-- 1. Composite index for RLS performance
CREATE INDEX IF NOT EXISTS "CompanyMission_organizationId_id_idx"
ON "CompanyMission" ("organizationId", "id");

-- 2. Enable RLS
ALTER TABLE "CompanyMission" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "CompanyMission" FORCE ROW LEVEL SECURITY;

-- 3. Grants
GRANT SELECT, INSERT, UPDATE, DELETE ON "CompanyMission" TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON "CompanyMission" TO admin_user;

-- 4. Policies (admin_user has BYPASSRLS — no policies needed for it)
CREATE POLICY companymission_select ON "CompanyMission"
FOR SELECT TO app_user
USING ("organizationId" = current_setting('app.current_org_id', true));

CREATE POLICY companymission_insert ON "CompanyMission"
FOR INSERT TO app_user
WITH CHECK ("organizationId" = current_setting('app.current_org_id', true));

CREATE POLICY companymission_update ON "CompanyMission"
FOR UPDATE TO app_user
USING ("organizationId" = current_setting('app.current_org_id', true))
WITH CHECK ("organizationId" = current_setting('app.current_org_id', true));

CREATE POLICY companymission_delete ON "CompanyMission"
FOR DELETE TO app_user
USING ("organizationId" = current_setting('app.current_org_id', true));

-- =========================================================================
-- Rollback (commented — uncomment to revert)
-- =========================================================================
-- DROP POLICY IF EXISTS companymission_select ON "CompanyMission";
-- DROP POLICY IF EXISTS companymission_insert ON "CompanyMission";
-- DROP POLICY IF EXISTS companymission_update ON "CompanyMission";
-- DROP POLICY IF EXISTS companymission_delete ON "CompanyMission";
-- ALTER TABLE "CompanyMission" DISABLE ROW LEVEL SECURITY;
-- DROP INDEX IF EXISTS "CompanyMission_organizationId_id_idx";
17 changes: 11 additions & 6 deletions src/app/api/mission/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from "zod";
import { requireOrgMember, isAuthError } from "@/lib/api/auth-guard";
import { parseBodyWithLimit } from "@/lib/api/body-limit";
import { prisma } from "@/lib/prisma";
import { withOrgContext } from "@/lib/db/rls-middleware";
import { logger } from "@/lib/logger";

const UpsertMissionSchema = z.object({
Expand All @@ -23,7 +24,9 @@ export async function GET(request: NextRequest): Promise<NextResponse> {
if (isAuthError(authResult)) return authResult;

try {
const mission = await prisma.companyMission.findUnique({ where: { organizationId: orgId } });
const mission = await withOrgContext(prisma, orgId, (tx) =>
tx.companyMission.findUnique({ where: { organizationId: orgId } })
);
return NextResponse.json({ success: true, data: mission });
} catch (error) {
logger.error("GET /api/mission error", { orgId, error });
Expand Down Expand Up @@ -58,11 +61,13 @@ async function upsertMission(request: NextRequest): Promise<NextResponse> {
if (isAuthError(authResult)) return authResult;

try {
const mission = await prisma.companyMission.upsert({
where: { organizationId },
update: { statement, vision: vision ?? null, values: values ?? [] },
create: { organizationId, statement, vision: vision ?? null, values: values ?? [] },
});
const mission = await withOrgContext(prisma, organizationId, (tx) =>
tx.companyMission.upsert({
where: { organizationId },
update: { statement, vision: vision ?? null, values: values ?? [] },
create: { organizationId, statement, vision: vision ?? null, values: values ?? [] },
})
);

return NextResponse.json({ success: true, data: mission }, { status: 200 });
} catch (error) {
Expand Down
5 changes: 4 additions & 1 deletion src/lib/goals/goal-context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prisma } from "@/lib/prisma";
import { logger } from "@/lib/logger";
import { withOrgContext } from "@/lib/db/rls-middleware";
import { getAgentAncestors } from "@/lib/org-chart/hierarchy";
import type { CompanyMission } from "@/generated/prisma";
import type { RuntimeContext } from "@/lib/runtime/types";
Expand Down Expand Up @@ -75,7 +76,9 @@ export async function getAgentGoals(agentId: string): Promise<AgentGoalItem[]> {
}

export async function getMissionForOrg(organizationId: string): Promise<CompanyMission | null> {
return prisma.companyMission.findUnique({ where: { organizationId } });
return withOrgContext(prisma, organizationId, (tx) =>
tx.companyMission.findUnique({ where: { organizationId } })
);
}

export async function buildGoalPrompt(agentId: string): Promise<string> {
Expand Down
Loading