-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add row level security #25
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
Open
matteyu
wants to merge
13
commits into
arc-937/auth-passkeys
Choose a base branch
from
arc-1133/row-level-security
base: arc-937/auth-passkeys
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 3 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
5de4eee
feat: add rls modify prisma client
matteyu 033e994
chore: rename param
matteyu 5b81c48
check auth schema
matteyu ab54014
fix session creation
matteyu 685e50c
Merge branch 'development' of github.com:arconnectio/embed-api into a…
matteyu 23cd5a4
fix rls without bypass rls
matteyu fae084d
fix rls violations
matteyu d90d04f
remove awaits for db operations
matteyu 810319e
Merge branch 'development' of github.com:arconnectio/embed-api into a…
matteyu 1647b13
major refactor for rls
matteyu 43eedb3
fix: wallet recovery
matteyu 4e8d02f
fix: wallet operations
matteyu 7fd4e20
cleanup debug logs
matteyu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
281 changes: 281 additions & 0 deletions
281
prisma/migrations/20250402214855_enable_rls/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| -- Enable Row Level Security on all tables | ||
| ALTER TABLE "UserProfiles" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Bills" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Applications" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Wallets" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "WalletActivations" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "WalletRecoveries" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "WalletExports" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "WorkKeyShares" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "RecoveryKeyShares" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Challenges" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "AnonChallenges" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "DevicesAndLocations" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Sessions" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "ApplicationSessions" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "LoginAttempts" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Organizations" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Teams" ENABLE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Memberships" ENABLE ROW LEVEL SECURITY; | ||
|
|
||
| -- Force RLS even for superusers | ||
| ALTER TABLE "UserProfiles" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Bills" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Applications" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Wallets" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "WalletActivations" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "WalletRecoveries" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "WalletExports" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "WorkKeyShares" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "RecoveryKeyShares" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Challenges" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "AnonChallenges" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "DevicesAndLocations" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Sessions" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "ApplicationSessions" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "LoginAttempts" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Organizations" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Teams" FORCE ROW LEVEL SECURITY; | ||
| ALTER TABLE "Memberships" FORCE ROW LEVEL SECURITY; | ||
|
|
||
| -- Policy for service roles to access all tables | ||
| -- This creates a policy that allows users with the 'service_role' claim to access all data | ||
| CREATE POLICY "Service role access all UserProfiles" ON "UserProfiles" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all Bills" ON "Bills" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all Applications" ON "Applications" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all Wallets" ON "Wallets" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all WalletActivations" ON "WalletActivations" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all WalletRecoveries" ON "WalletRecoveries" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all WalletExports" ON "WalletExports" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all WorkKeyShares" ON "WorkKeyShares" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all RecoveryKeyShares" ON "RecoveryKeyShares" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all Challenges" ON "Challenges" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all AnonChallenges" ON "AnonChallenges" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all DevicesAndLocations" ON "DevicesAndLocations" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all Sessions" ON "Sessions" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all ApplicationSessions" ON "ApplicationSessions" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all LoginAttempts" ON "LoginAttempts" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all Organizations" ON "Organizations" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all Teams" ON "Teams" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
| CREATE POLICY "Service role access all Memberships" ON "Memberships" | ||
| USING (current_setting('request.jwt.claims', true)::json->>'role' = 'service_role'); | ||
|
|
||
| DO $$ | ||
| BEGIN | ||
| IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN | ||
| EXECUTE $auth_policies$ | ||
| -- User-specific policies | ||
| -- UserProfiles | ||
| CREATE POLICY "Users can view their own profile" ON "UserProfiles" | ||
| FOR SELECT USING (auth.uid() = "supId"); | ||
| CREATE POLICY "Users can update their own profile" ON "UserProfiles" | ||
| FOR UPDATE USING (auth.uid() = "supId"); | ||
|
|
||
| -- Wallets | ||
| CREATE POLICY "Users can view their own wallets" ON "Wallets" | ||
| FOR SELECT USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can update their own wallets" ON "Wallets" | ||
| FOR UPDATE USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can insert their own wallets" ON "Wallets" | ||
| FOR INSERT WITH CHECK (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can delete their own wallets" ON "Wallets" | ||
| FOR DELETE USING (auth.uid() = "userId"); | ||
|
|
||
| -- WalletActivations | ||
| CREATE POLICY "Users can view their own wallet activations" ON "WalletActivations" | ||
| FOR SELECT USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can insert their own wallet activations" ON "WalletActivations" | ||
| FOR INSERT WITH CHECK (auth.uid() = "userId"); | ||
|
|
||
| -- WalletRecoveries | ||
| CREATE POLICY "Users can view their own wallet recoveries" ON "WalletRecoveries" | ||
| FOR SELECT USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can insert their own wallet recoveries" ON "WalletRecoveries" | ||
| FOR INSERT WITH CHECK (auth.uid() = "userId"); | ||
|
|
||
| -- WalletExports | ||
| CREATE POLICY "Users can view their own wallet exports" ON "WalletExports" | ||
| FOR SELECT USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can insert their own wallet exports" ON "WalletExports" | ||
| FOR INSERT WITH CHECK (auth.uid() = "userId"); | ||
|
|
||
| -- WorkKeyShares | ||
| CREATE POLICY "Users can view their own work key shares" ON "WorkKeyShares" | ||
| FOR SELECT USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can insert their own work key shares" ON "WorkKeyShares" | ||
| FOR INSERT WITH CHECK (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can update their own work key shares" ON "WorkKeyShares" | ||
| FOR UPDATE USING (auth.uid() = "userId"); | ||
|
|
||
| -- RecoveryKeyShares | ||
| CREATE POLICY "Users can view their own recovery key shares" ON "RecoveryKeyShares" | ||
| FOR SELECT USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can insert their own recovery key shares" ON "RecoveryKeyShares" | ||
| FOR INSERT WITH CHECK (auth.uid() = "userId"); | ||
|
|
||
| -- Challenges | ||
| CREATE POLICY "Users can view their own challenges" ON "Challenges" | ||
| FOR SELECT USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can insert their own challenges" ON "Challenges" | ||
| FOR INSERT WITH CHECK (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can update their own challenges" ON "Challenges" | ||
| FOR UPDATE USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can delete their own challenges" ON "Challenges" | ||
| FOR DELETE USING (auth.uid() = "userId"); | ||
|
|
||
| -- DevicesAndLocations | ||
matteyu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| CREATE POLICY "Users can view their own devices and locations" ON "DevicesAndLocations" | ||
| FOR SELECT USING (auth.uid() = "userId" OR "userId" IS NULL); | ||
| CREATE POLICY "Users can insert devices and locations" ON "DevicesAndLocations" | ||
| FOR INSERT WITH CHECK (auth.uid() = "userId" OR "userId" IS NULL); | ||
|
|
||
| -- Sessions | ||
| CREATE POLICY "Users can view their own sessions" ON "Sessions" | ||
| FOR SELECT USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can update their own sessions" ON "Sessions" | ||
| FOR UPDATE USING (auth.uid() = "userId"); | ||
| CREATE POLICY "Users can delete their own sessions" ON "Sessions" | ||
| FOR DELETE USING (auth.uid() = "userId"); | ||
|
|
||
| -- Membership-related policies | ||
| -- Organizations | ||
| CREATE POLICY "Users can view organizations they are members of" ON "Organizations" | ||
| FOR SELECT USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."organizationId" = "Organizations"."id" | ||
| AND "Memberships"."userId" = auth.uid() | ||
| ) | ||
| ); | ||
|
|
||
| CREATE POLICY "Users with owner role can update their organizations" ON "Organizations" | ||
| FOR UPDATE USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."organizationId" = "Organizations"."id" | ||
| AND "Memberships"."userId" = auth.uid() | ||
| AND "Memberships"."role" = 'OWNER' | ||
| ) | ||
| ); | ||
|
|
||
| -- Teams | ||
| CREATE POLICY "Users can view teams they are members of" ON "Teams" | ||
| FOR SELECT USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."teamId" = "Teams"."id" | ||
| AND "Memberships"."userId" = auth.uid() | ||
| ) | ||
| ); | ||
|
|
||
| CREATE POLICY "Users with owner/admin role can update their teams" ON "Teams" | ||
| FOR UPDATE USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."teamId" = "Teams"."id" | ||
| AND "Memberships"."userId" = auth.uid() | ||
| AND "Memberships"."role" IN ('OWNER', 'ADMIN') | ||
| ) | ||
| ); | ||
|
|
||
| -- Applications | ||
| CREATE POLICY "Team members can view applications" ON "Applications" | ||
| FOR SELECT USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."userId" = auth.uid() | ||
| AND "Memberships"."teamId" = "Applications"."teamId" | ||
| ) | ||
| ); | ||
|
|
||
| CREATE POLICY "Team owners/admins can insert applications" ON "Applications" | ||
| FOR INSERT WITH CHECK ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."userId" = auth.uid() | ||
| AND "Memberships"."teamId" = "Applications"."teamId" | ||
| AND "Memberships"."role" IN ('OWNER', 'ADMIN') | ||
| ) | ||
| ); | ||
|
|
||
| CREATE POLICY "Team owners/admins can update applications" ON "Applications" | ||
| FOR UPDATE USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."userId" = auth.uid() | ||
| AND "Memberships"."teamId" = "Applications"."teamId" | ||
| AND "Memberships"."role" IN ('OWNER', 'ADMIN') | ||
| ) | ||
| ); | ||
|
|
||
| CREATE POLICY "Team owners/admins can delete applications" ON "Applications" | ||
| FOR DELETE USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."userId" = auth.uid() | ||
| AND "Memberships"."teamId" = "Applications"."teamId" | ||
| AND "Memberships"."role" IN ('OWNER', 'ADMIN') | ||
| ) | ||
| ); | ||
|
|
||
| -- Memberships | ||
| CREATE POLICY "Users can view memberships they are part of" ON "Memberships" | ||
| FOR SELECT USING ( | ||
| "userId" = auth.uid() OR | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" AS m | ||
| WHERE m."userId" = auth.uid() | ||
| AND m."organizationId" = "Memberships"."organizationId" | ||
| ) | ||
| ); | ||
|
|
||
| CREATE POLICY "Team owners can manage memberships" ON "Memberships" | ||
| FOR ALL USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" AS m | ||
| WHERE m."userId" = auth.uid() | ||
| AND m."organizationId" = "Memberships"."organizationId" | ||
| AND m."teamId" = "Memberships"."teamId" | ||
| AND m."role" = 'OWNER' | ||
| ) | ||
| ); | ||
|
|
||
| -- Bills | ||
| CREATE POLICY "Users can view bills for their organizations" ON "Bills" | ||
| FOR SELECT USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Memberships" | ||
| WHERE "Memberships"."userId" = auth.uid() | ||
| AND "Memberships"."organizationId" = "Bills"."organizationId" | ||
| ) | ||
| ); | ||
|
|
||
| -- Application Sessions | ||
| CREATE POLICY "Users can view their own application sessions" ON "ApplicationSessions" | ||
| FOR SELECT USING ( | ||
| EXISTS ( | ||
| SELECT 1 FROM "Sessions" | ||
| WHERE "Sessions"."id" = "ApplicationSessions"."sessionId" | ||
| AND "Sessions"."userId" = auth.uid() | ||
| ) | ||
| ); | ||
| $auth_policies$; | ||
| END IF; | ||
| END $$; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,41 @@ | ||
| import { PrismaClient } from "@prisma/client"; | ||
|
|
||
| // Create a base Prisma client | ||
| const globalForPrisma = global as unknown as { prisma: PrismaClient }; | ||
| export const basePrisma = globalForPrisma.prisma || new PrismaClient(); | ||
|
|
||
| export const prisma = globalForPrisma.prisma || new PrismaClient(); | ||
| if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = basePrisma; | ||
|
|
||
| if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; | ||
| // Function to create an authenticated Prisma client instance | ||
| export function createAuthenticatedPrismaClient(userId?: string, role?: string) { | ||
| // User ID is required for authenticated client | ||
| // It should be extracted from the JWT token | ||
| if (!userId && !role) { | ||
| return basePrisma; | ||
| } | ||
|
|
||
| // Create a new client with extensions to handle authentication | ||
| const prisma = new PrismaClient({ | ||
matteyu marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| datasources: { | ||
| db: { | ||
| url: process.env.DATABASE_URL, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| return prisma.$extends({ | ||
| client: { | ||
| async $beforeQuery() { | ||
| // Set PostgreSQL role and JWT claims via raw queries before each operation | ||
| if (role) { | ||
| await prisma.$executeRawUnsafe(`SET ROLE ${role}`); | ||
| } | ||
|
|
||
| if (userId) { | ||
| const claimsJson = JSON.stringify({ sub: userId, role }); | ||
| await prisma.$executeRawUnsafe(`SET LOCAL "request.jwt.claims" = '${claimsJson}'`); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.