Skip to content

Commit 5de4eee

Browse files
committed
feat: add rls modify prisma client
1 parent ec14eb3 commit 5de4eee

File tree

4 files changed

+328
-7
lines changed

4 files changed

+328
-7
lines changed
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
-- Enable Row Level Security on all tables
2+
ALTER TABLE "UserProfiles" ENABLE ROW LEVEL SECURITY;
3+
ALTER TABLE "Bills" ENABLE ROW LEVEL SECURITY;
4+
ALTER TABLE "Applications" ENABLE ROW LEVEL SECURITY;
5+
ALTER TABLE "Wallets" ENABLE ROW LEVEL SECURITY;
6+
ALTER TABLE "WalletActivations" ENABLE ROW LEVEL SECURITY;
7+
ALTER TABLE "WalletRecoveries" ENABLE ROW LEVEL SECURITY;
8+
ALTER TABLE "WalletExports" ENABLE ROW LEVEL SECURITY;
9+
ALTER TABLE "WorkKeyShares" ENABLE ROW LEVEL SECURITY;
10+
ALTER TABLE "RecoveryKeyShares" ENABLE ROW LEVEL SECURITY;
11+
ALTER TABLE "Challenges" ENABLE ROW LEVEL SECURITY;
12+
ALTER TABLE "AnonChallenges" ENABLE ROW LEVEL SECURITY;
13+
ALTER TABLE "DevicesAndLocations" ENABLE ROW LEVEL SECURITY;
14+
ALTER TABLE "Sessions" ENABLE ROW LEVEL SECURITY;
15+
ALTER TABLE "ApplicationSessions" ENABLE ROW LEVEL SECURITY;
16+
ALTER TABLE "LoginAttempts" ENABLE ROW LEVEL SECURITY;
17+
ALTER TABLE "Organizations" ENABLE ROW LEVEL SECURITY;
18+
ALTER TABLE "Teams" ENABLE ROW LEVEL SECURITY;
19+
ALTER TABLE "Memberships" ENABLE ROW LEVEL SECURITY;
20+
21+
-- Force RLS even for superusers
22+
ALTER TABLE "UserProfiles" FORCE ROW LEVEL SECURITY;
23+
ALTER TABLE "Bills" FORCE ROW LEVEL SECURITY;
24+
ALTER TABLE "Applications" FORCE ROW LEVEL SECURITY;
25+
ALTER TABLE "Wallets" FORCE ROW LEVEL SECURITY;
26+
ALTER TABLE "WalletActivations" FORCE ROW LEVEL SECURITY;
27+
ALTER TABLE "WalletRecoveries" FORCE ROW LEVEL SECURITY;
28+
ALTER TABLE "WalletExports" FORCE ROW LEVEL SECURITY;
29+
ALTER TABLE "WorkKeyShares" FORCE ROW LEVEL SECURITY;
30+
ALTER TABLE "RecoveryKeyShares" FORCE ROW LEVEL SECURITY;
31+
ALTER TABLE "Challenges" FORCE ROW LEVEL SECURITY;
32+
ALTER TABLE "AnonChallenges" FORCE ROW LEVEL SECURITY;
33+
ALTER TABLE "DevicesAndLocations" FORCE ROW LEVEL SECURITY;
34+
ALTER TABLE "Sessions" FORCE ROW LEVEL SECURITY;
35+
ALTER TABLE "ApplicationSessions" FORCE ROW LEVEL SECURITY;
36+
ALTER TABLE "LoginAttempts" FORCE ROW LEVEL SECURITY;
37+
ALTER TABLE "Organizations" FORCE ROW LEVEL SECURITY;
38+
ALTER TABLE "Teams" FORCE ROW LEVEL SECURITY;
39+
ALTER TABLE "Memberships" FORCE ROW LEVEL SECURITY;
40+
41+
-- Policy for service roles to access all tables
42+
-- This creates a policy that allows users with the 'service_role' claim to access all data
43+
CREATE POLICY "Service role access all UserProfiles" ON "UserProfiles"
44+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
45+
CREATE POLICY "Service role access all Bills" ON "Bills"
46+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
47+
CREATE POLICY "Service role access all Applications" ON "Applications"
48+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
49+
CREATE POLICY "Service role access all Wallets" ON "Wallets"
50+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
51+
CREATE POLICY "Service role access all WalletActivations" ON "WalletActivations"
52+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
53+
CREATE POLICY "Service role access all WalletRecoveries" ON "WalletRecoveries"
54+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
55+
CREATE POLICY "Service role access all WalletExports" ON "WalletExports"
56+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
57+
CREATE POLICY "Service role access all WorkKeyShares" ON "WorkKeyShares"
58+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
59+
CREATE POLICY "Service role access all RecoveryKeyShares" ON "RecoveryKeyShares"
60+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
61+
CREATE POLICY "Service role access all Challenges" ON "Challenges"
62+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
63+
CREATE POLICY "Service role access all AnonChallenges" ON "AnonChallenges"
64+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
65+
CREATE POLICY "Service role access all DevicesAndLocations" ON "DevicesAndLocations"
66+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
67+
CREATE POLICY "Service role access all Sessions" ON "Sessions"
68+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
69+
CREATE POLICY "Service role access all ApplicationSessions" ON "ApplicationSessions"
70+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
71+
CREATE POLICY "Service role access all LoginAttempts" ON "LoginAttempts"
72+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
73+
CREATE POLICY "Service role access all Organizations" ON "Organizations"
74+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
75+
CREATE POLICY "Service role access all Teams" ON "Teams"
76+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
77+
CREATE POLICY "Service role access all Memberships" ON "Memberships"
78+
USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role');
79+
80+
-- User-specific policies
81+
-- UserProfiles
82+
CREATE POLICY "Users can view their own profile" ON "UserProfiles"
83+
FOR SELECT USING (auth.uid() = "supId");
84+
CREATE POLICY "Users can update their own profile" ON "UserProfiles"
85+
FOR UPDATE USING (auth.uid() = "supId");
86+
87+
-- Wallets
88+
CREATE POLICY "Users can view their own wallets" ON "Wallets"
89+
FOR SELECT USING (auth.uid() = "userId");
90+
CREATE POLICY "Users can update their own wallets" ON "Wallets"
91+
FOR UPDATE USING (auth.uid() = "userId");
92+
CREATE POLICY "Users can insert their own wallets" ON "Wallets"
93+
FOR INSERT WITH CHECK (auth.uid() = "userId");
94+
CREATE POLICY "Users can delete their own wallets" ON "Wallets"
95+
FOR DELETE USING (auth.uid() = "userId");
96+
97+
-- WalletActivations
98+
CREATE POLICY "Users can view their own wallet activations" ON "WalletActivations"
99+
FOR SELECT USING (auth.uid() = "userId");
100+
CREATE POLICY "Users can insert their own wallet activations" ON "WalletActivations"
101+
FOR INSERT WITH CHECK (auth.uid() = "userId");
102+
103+
-- WalletRecoveries
104+
CREATE POLICY "Users can view their own wallet recoveries" ON "WalletRecoveries"
105+
FOR SELECT USING (auth.uid() = "userId");
106+
CREATE POLICY "Users can insert their own wallet recoveries" ON "WalletRecoveries"
107+
FOR INSERT WITH CHECK (auth.uid() = "userId");
108+
109+
-- WalletExports
110+
CREATE POLICY "Users can view their own wallet exports" ON "WalletExports"
111+
FOR SELECT USING (auth.uid() = "userId");
112+
CREATE POLICY "Users can insert their own wallet exports" ON "WalletExports"
113+
FOR INSERT WITH CHECK (auth.uid() = "userId");
114+
115+
-- WorkKeyShares
116+
CREATE POLICY "Users can view their own work key shares" ON "WorkKeyShares"
117+
FOR SELECT USING (auth.uid() = "userId");
118+
CREATE POLICY "Users can insert their own work key shares" ON "WorkKeyShares"
119+
FOR INSERT WITH CHECK (auth.uid() = "userId");
120+
CREATE POLICY "Users can update their own work key shares" ON "WorkKeyShares"
121+
FOR UPDATE USING (auth.uid() = "userId");
122+
123+
-- RecoveryKeyShares
124+
CREATE POLICY "Users can view their own recovery key shares" ON "RecoveryKeyShares"
125+
FOR SELECT USING (auth.uid() = "userId");
126+
CREATE POLICY "Users can insert their own recovery key shares" ON "RecoveryKeyShares"
127+
FOR INSERT WITH CHECK (auth.uid() = "userId");
128+
129+
-- Challenges
130+
CREATE POLICY "Users can view their own challenges" ON "Challenges"
131+
FOR SELECT USING (auth.uid() = "userId");
132+
CREATE POLICY "Users can insert their own challenges" ON "Challenges"
133+
FOR INSERT WITH CHECK (auth.uid() = "userId");
134+
CREATE POLICY "Users can update their own challenges" ON "Challenges"
135+
FOR UPDATE USING (auth.uid() = "userId");
136+
CREATE POLICY "Users can delete their own challenges" ON "Challenges"
137+
FOR DELETE USING (auth.uid() = "userId");
138+
139+
-- DevicesAndLocations
140+
CREATE POLICY "Users can view their own devices and locations" ON "DevicesAndLocations"
141+
FOR SELECT USING (auth.uid() = "userId" OR "userId" IS NULL);
142+
CREATE POLICY "Users can insert devices and locations" ON "DevicesAndLocations"
143+
FOR INSERT WITH CHECK (auth.uid() = "userId" OR "userId" IS NULL);
144+
145+
-- Sessions
146+
CREATE POLICY "Users can view their own sessions" ON "Sessions"
147+
FOR SELECT USING (auth.uid() = "userId");
148+
CREATE POLICY "Users can update their own sessions" ON "Sessions"
149+
FOR UPDATE USING (auth.uid() = "userId");
150+
CREATE POLICY "Users can delete their own sessions" ON "Sessions"
151+
FOR DELETE USING (auth.uid() = "userId");
152+
153+
-- Membership-related policies
154+
-- Organizations
155+
CREATE POLICY "Users can view organizations they are members of" ON "Organizations"
156+
FOR SELECT USING (
157+
EXISTS (
158+
SELECT 1 FROM "Memberships"
159+
WHERE "Memberships"."organizationId" = "Organizations"."id"
160+
AND "Memberships"."userId" = auth.uid()
161+
)
162+
);
163+
164+
CREATE POLICY "Users with owner role can update their organizations" ON "Organizations"
165+
FOR UPDATE USING (
166+
EXISTS (
167+
SELECT 1 FROM "Memberships"
168+
WHERE "Memberships"."organizationId" = "Organizations"."id"
169+
AND "Memberships"."userId" = auth.uid()
170+
AND "Memberships"."role" = 'OWNER'
171+
)
172+
);
173+
174+
-- Teams
175+
CREATE POLICY "Users can view teams they are members of" ON "Teams"
176+
FOR SELECT USING (
177+
EXISTS (
178+
SELECT 1 FROM "Memberships"
179+
WHERE "Memberships"."teamId" = "Teams"."id"
180+
AND "Memberships"."userId" = auth.uid()
181+
)
182+
);
183+
184+
CREATE POLICY "Users with owner/admin role can update their teams" ON "Teams"
185+
FOR UPDATE USING (
186+
EXISTS (
187+
SELECT 1 FROM "Memberships"
188+
WHERE "Memberships"."teamId" = "Teams"."id"
189+
AND "Memberships"."userId" = auth.uid()
190+
AND "Memberships"."role" IN ('OWNER', 'ADMIN')
191+
)
192+
);
193+
194+
-- Applications
195+
CREATE POLICY "Team members can view applications" ON "Applications"
196+
FOR SELECT USING (
197+
EXISTS (
198+
SELECT 1 FROM "Memberships"
199+
WHERE "Memberships"."userId" = auth.uid()
200+
AND "Memberships"."teamId" = "Applications"."teamId"
201+
)
202+
);
203+
204+
CREATE POLICY "Team owners/admins can insert applications" ON "Applications"
205+
FOR INSERT WITH CHECK (
206+
EXISTS (
207+
SELECT 1 FROM "Memberships"
208+
WHERE "Memberships"."userId" = auth.uid()
209+
AND "Memberships"."teamId" = NEW."teamId"
210+
AND "Memberships"."role" IN ('OWNER', 'ADMIN')
211+
)
212+
);
213+
214+
CREATE POLICY "Team owners/admins can update applications" ON "Applications"
215+
FOR UPDATE USING (
216+
EXISTS (
217+
SELECT 1 FROM "Memberships"
218+
WHERE "Memberships"."userId" = auth.uid()
219+
AND "Memberships"."teamId" = "Applications"."teamId"
220+
AND "Memberships"."role" IN ('OWNER', 'ADMIN')
221+
)
222+
);
223+
224+
CREATE POLICY "Team owners/admins can delete applications" ON "Applications"
225+
FOR DELETE USING (
226+
EXISTS (
227+
SELECT 1 FROM "Memberships"
228+
WHERE "Memberships"."userId" = auth.uid()
229+
AND "Memberships"."teamId" = "Applications"."teamId"
230+
AND "Memberships"."role" IN ('OWNER', 'ADMIN')
231+
)
232+
);
233+
234+
-- Memberships
235+
CREATE POLICY "Users can view memberships they are part of" ON "Memberships"
236+
FOR SELECT USING (
237+
"userId" = auth.uid() OR
238+
EXISTS (
239+
SELECT 1 FROM "Memberships" AS m
240+
WHERE m."userId" = auth.uid()
241+
AND m."organizationId" = "Memberships"."organizationId"
242+
)
243+
);
244+
245+
CREATE POLICY "Team owners can manage memberships" ON "Memberships"
246+
FOR ALL USING (
247+
EXISTS (
248+
SELECT 1 FROM "Memberships" AS m
249+
WHERE m."userId" = auth.uid()
250+
AND m."organizationId" = "Memberships"."organizationId"
251+
AND m."teamId" = "Memberships"."teamId"
252+
AND m."role" = 'OWNER'
253+
)
254+
);
255+
256+
-- Bills
257+
CREATE POLICY "Users can view bills for their organizations" ON "Bills"
258+
FOR SELECT USING (
259+
EXISTS (
260+
SELECT 1 FROM "Memberships"
261+
WHERE "Memberships"."userId" = auth.uid()
262+
AND "Memberships"."organizationId" = "Bills"."organizationId"
263+
)
264+
);
265+
266+
-- Application Sessions
267+
CREATE POLICY "Users can view their own application sessions" ON "ApplicationSessions"
268+
FOR SELECT USING (
269+
EXISTS (
270+
SELECT 1 FROM "Sessions"
271+
WHERE "Sessions"."id" = "ApplicationSessions"."sessionId"
272+
AND "Sessions"."userId" = auth.uid()
273+
)
274+
);

server/context.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { inferAsyncReturnType } from "@trpc/server";
22
import { Session } from "@prisma/client";
33
import { createServerClient } from "@/server/utils/supabase/supabase-server-client";
44
import { jwtDecode } from "jwt-decode";
5-
import { prisma } from "./utils/prisma/prisma-client";
5+
import { basePrisma, createAuthenticatedPrismaClient } from "./utils/prisma/prisma-client";
66
import {
77
getClientCountryCode,
88
getClientIp,
@@ -14,6 +14,9 @@ export async function createContext({ req }: { req: Request }) {
1414
const clientId = req.headers.get("x-client-id");
1515
const applicationId = req.headers.get("x-application-id") || "";
1616

17+
// Default to using unauthenticated prisma client
18+
let prisma = basePrisma;
19+
1720
if (!authHeader || !clientId) {
1821
return createEmptyContext();
1922
}
@@ -42,6 +45,11 @@ export async function createContext({ req }: { req: Request }) {
4245
}
4346

4447
const user = data.user;
48+
49+
// Create an authenticated Prisma client with the user's JWT token
50+
// Pass 'authenticated' role to activate RLS policies
51+
prisma = createAuthenticatedPrismaClient(user.id, 'authenticated');
52+
4553
let ip = getClientIp(req);
4654

4755
if (process.env.NODE_ENV === "development") {
@@ -91,7 +99,10 @@ async function getAndUpdateSession(
9199
if (Object.keys(sessionUpdates).length > 0) {
92100
console.log("Updating session:", sessionUpdates);
93101

94-
prisma.session
102+
// Use authenticated prisma client for the current user
103+
const authPrisma = createAuthenticatedPrismaClient(userId, 'authenticated');
104+
105+
authPrisma.session
95106
.update({
96107
where: { id: sessionId },
97108
data: sessionUpdates,
@@ -119,7 +130,7 @@ function decodeJwt(token: string) {
119130

120131
function createEmptyContext() {
121132
return {
122-
prisma,
133+
prisma: basePrisma, // Use base prisma client for unauthenticated requests
123134
user: null,
124135
session: createSessionObject(null),
125136
};
Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,40 @@
11
import { PrismaClient } from "@prisma/client";
22

3+
// Create a base Prisma client
34
const globalForPrisma = global as unknown as { prisma: PrismaClient };
5+
export const basePrisma = globalForPrisma.prisma || new PrismaClient();
46

5-
export const prisma = globalForPrisma.prisma || new PrismaClient();
7+
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = basePrisma;
68

7-
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
9+
// Function to create an authenticated Prisma client instance
10+
export function createAuthenticatedPrismaClient(jwtToken?: string, role?: string) {
11+
// If no token, return the base client
12+
if (!jwtToken && !role) {
13+
return basePrisma;
14+
}
15+
16+
// Create a new client with extensions to handle authentication
17+
const prisma = new PrismaClient({
18+
datasources: {
19+
db: {
20+
url: process.env.DATABASE_URL,
21+
},
22+
},
23+
});
24+
25+
return prisma.$extends({
26+
client: {
27+
async $beforeQuery() {
28+
// Set PostgreSQL role and JWT claims via raw queries before each operation
29+
if (role) {
30+
await prisma.$executeRawUnsafe(`SET ROLE ${role}`);
31+
}
32+
33+
if (jwtToken) {
34+
const claimsJson = JSON.stringify({ sub: jwtToken, role });
35+
await prisma.$executeRawUnsafe(`SET LOCAL "request.jwt.claims" = '${claimsJson}'`);
36+
}
37+
}
38+
}
39+
});
40+
}

server/utils/validation/validation.utils.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { prisma } from "../prisma/prisma-client";
1+
import { createAuthenticatedPrismaClient } from "../prisma/prisma-client";
22
import { TRPCError } from "@trpc/server";
33

44
function extractDomain(origin: string | null): string | null {
@@ -30,9 +30,12 @@ function isDomainAllowed(
3030
export async function validateApplication(
3131
clientId: string,
3232
origin: string,
33-
sessionId?: string
33+
sessionId?: string,
34+
userId?: string
3435
): Promise<string> {
3536
try {
37+
const prisma = createAuthenticatedPrismaClient(userId, 'authenticated');
38+
3639
const application = await prisma.application
3740
.findUnique({
3841
where: { clientId },

0 commit comments

Comments
 (0)