Replies: 60 comments 20 replies
-
There doesn't seem to be a clean escape hatch in either schema or migrations to enable this too, making it even more important. |
Beta Was this translation helpful? Give feedback.
-
Related issue in prisma repo prisma/prisma#12735, look like there is already a way how to do it, maybe there are some tips to go forward. |
Beta Was this translation helpful? Give feedback.
-
After doing some work w/ drizzle for a while, you can indeed emulate RLS (and by extension Supabase) via manually hand editing Migration files & using transactions to set contextual data. It's not great, but it does work. |
Beta Was this translation helpful? Give feedback.
-
do you have an example ? |
Beta Was this translation helpful? Give feedback.
-
+1 for this |
Beta Was this translation helpful? Give feedback.
-
This is how I'm currently doing it (I'm using Supabase): rls.sql DO $$
DECLARE t text;
BEGIN
FOR t IN
SELECT table_name
FROM information_schema.columns
WHERE table_schema = 'public'
LOOP
EXECUTE format('
ALTER TABLE %I ENABLE ROW LEVEL SECURITY;
', t, t);
END loop;
END;
$$ LANGUAGE plpgsql; generate_migrations.sh pnpm run db:generate-schema
file_name=$(pnpm run db:generate-custom | grep -oP '(?<=\b)\S+\.sql\b') && cat ./src/custom-sql/rls.sql >> "$file_name" package.json "scripts": {
"db:generate-custom": "drizzle-kit generate:pg --custom",
"db:generate-schema": "drizzle-kit generate:pg",
"db:generate-all": "sh ./scripts/generate_migrations.sh",
"db:generate-clean": "rm -rf ./migrations && pnpm run db:generate-all",
} |
Beta Was this translation helpful? Give feedback.
-
For those who has this problem, for now I am using this function helper so I can emulate RLS for supabase. import { type Session } from "@supabase/supabase-js"
import { sql } from "drizzle-orm"
import { PostgresJsDatabase, drizzle } from "drizzle-orm/postgres-js"
import postgres from "postgres"
const queryConnection = postgres(process.env.DATABASE_URL!, {
connection: {},
})
export const db = drizzle(queryConnection)
export function authDB<T>(
session: Session,
cb: (sql: PostgresJsDatabase) => T | Promise<T>
): Promise<T> {
// You can add a validation here for the accessToken - we rely on supabase for now
const jwtClaim = decodeJwt(session.access_token)
const role = JSON.parse(jwtClaim).role
return db.transaction(async (tx) => {
// Set JWT to enable RLS. supabase adds the role and the userId (sub) to the jwt claims
await tx.execute(
sql`SELECT set_config('request.jwt.claims', '${sql.raw(jwtClaim)}', TRUE)`
)
// do not use postgres because it will bypass the RLS, set role to authenticated
await tx.execute(sql`set role '${sql.raw(role)}'`)
return cb(tx)
}) as Promise<T>
} Then you can use that helper for querying your db: const {
data: { session },
} = await supabaseClient.auth.getSession()
if (session) {
const data = await authDB(session, (tx) => {
// This will be executed in an authenticated context.
return tx.select().from(organization_profiles)
})
} Please let me know if you catch any problem. This comment was very useful porsager/postgres#559 (comment) thanks! |
Beta Was this translation helpful? Give feedback.
-
I understand the desire for native RLS support in Drizzle for a more streamlined and unified approach. However, as an interim solution, couldn't we leverage the strengths of various tools to build a multi-layered security model? In my own setup, I have several layers of security checks:
By doing so, we can manage Row-Level Security via Supabase, while Drizzle can focus on other CRUD operations. This multi-tiered security approach isn't a direct answer to the request for native RLS in Drizzle, but it serves as a practical, if not ideal, workaround for those who need advanced security features now and can't wait for Drizzle to support them natively. |
Beta Was this translation helpful? Give feedback.
-
Hey Supabase lovers, can you look at the draft PR from Angelelz? |
Beta Was this translation helpful? Give feedback.
-
@Angelelz do you think we can add // Get a session from your code
const session = await getSession();
const jwtClaim = decodeJwt(session.access_token);
const role = JSON.parse(jwtClaim).role;
const data = await db.transaction(
async (tx) => {
// Drizzle has applied the config for you
// You now rely on RLS
await tx.select();
await tx.update();
// ...
},
{
configs: [
{
name: "request.jwt.claims",
value: jwtClaim,
isLocal: true,
},
],
role,
}
); It could let us writing custom helpers like: async function authenticatedOnly(){
const session = await getSession();
const jwtClaim = decodeJwt(session.access_token)
const role = JSON.parse(jwtClaim).role
return {
configs: [
{
name: "request.jwt.claims",
value: jwtClaim,
isLocal: true,
},
],
roles,
} satisfies PgTransactionOptions
}
const data = await db.transaction(
async (tx) => {
// Drizzle has applied the config for you
// You now rely on RLS
await tx.select(...);
await tx.update(...);
// ...
},
await authenticatedOnly()
); Of course, we will have to run |
Beta Was this translation helpful? Give feedback.
-
Since a transaction already accepts a configuration object, I would think that's the best place to put it. |
Beta Was this translation helpful? Give feedback.
-
So with all you share: Supabase RLS Policy functions and Postgres transaction configuration association
These user datas can come from Supabase auth or a user table. Use Supabase RLS with Drizzle TransactionYou have to use a transaction to isolate the user queries const data = await db.transaction(
async (tx) => {
// You use `auth.jwt()` in your RLS policy
await tx.execute(sql`SELECT set_config('request.jwt.claims', '${sql.raw(jwtClaim)}', TRUE)`);
// You use `auth.uid()` in your RLS policy
await tx.execute(sql`SELECT set_config('request.jwt.claim.sub', '${sql.raw(userUid)}', TRUE)`);
// You use `auth.email()` in your RLS policy
await tx.execute(sql`SELECT set_config('request.jwt.claim.email', '${sql.raw(userEmail)}', TRUE)`);
// You use `auth.role()` in your RLS policy
await tx.execute(sql`SELECT set_config('request.jwt.claim.role', '${sql.raw(userRole)}', TRUE)`)
// do not use the default role (Drizzle uses your root user with `postgres` role) because it will bypass the RLS policy, set role to authenticated
await tx.execute(sql`set local role authenticated`);
// All the following will be run with the user context set with `set_config`
await tx.select(...);
await tx.update(...);
// ...
}
); Maybe what Drizzle can doconst data = await db.transaction(
async (tx) => {
// Drizzle has applied the configuration for you
// All the following will be run with the user context set by Drizzle with `set_config`
await tx.select(...);
await tx.update(...);
// ...
},
{
configs: [
// You use `auth.jwt()` in your RLS policy
{
name: "request.jwt.claims",
value: jwtClaim,
isLocal: true,
},
// You use `auth.uid()` in your RLS policy
{
name: "request.jwt.claims.sub",
value: userUid,
isLocal: true,
},
// You use `auth.email()` in your RLS policy
{
name: "request.jwt.claims.email",
value: userEmail,
isLocal: true,
},
// You use `auth.role()` in your RLS policy
{
name: "request.jwt.claims.role",
value: userRole,
isLocal: true,
},
],
role: 'authenticated',
}
); Open for custom configuration helpersasync function authenticated(){
// Your own way to get the current user session, depending on what framework you use
const session = await getSession();
const jwtClaim = decodeJwt(session.access_token);
const role = session.user.role;
const userUid = session.user.sub;
const userEmail = session.user.email;
const userRole = session.user.role;
return {
configs: [
// You use `auth.jwt()` in your RLS policy
{
name: "request.jwt.claims",
value: jwtClaim,
isLocal: true,
},
// You use `auth.uid()` in your RLS policy
{
name: "request.jwt.claims.sub",
value: userUid,
isLocal: true,
},
// You use `auth.email()` in your RLS policy
{
name: "request.jwt.claims.email",
value: userEmail,
isLocal: true,
},
// You use `auth.role()` in your RLS policy
{
name: "request.jwt.claims.role",
value: userRole,
isLocal: true,
},
],
role,
} satisfies PgTransactionOptions
}
const data = await db.transaction(
async (tx) => {
// Drizzle has applied the configuration for you
// All the following will be run with the user context set by Drizzle with `set_config`
await tx.select(...);
await tx.update(...);
// ...
},
await authenticated()
); |
Beta Was this translation helpful? Give feedback.
-
@rphlmr Thank you for your research and compiling this for us!
type PgTransactionOptions = {
isolationLevel?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable';
accessMode?: 'read only' | 'read write';
deferrable?: boolean;
rls?: // All RLS config should go here
}
const data = await db.transaction(
async (tx) => {
// Drizzle has applied the configuration for you
// All the following will be run with the user context set by Drizzle with `set_config`
await tx.select(...);
await tx.update(...);
// ...
},
authenticated
); Any supabase/RLS user: any feedback will be appreciated. |
Beta Was this translation helpful? Give feedback.
-
I think Drizzle could iterate over the
Anything that can work. Can't say which is better. const stringToken = JSON.stringify({ id: "1" });
const pgDialect = new PgDialect();
const query1 = sql`SELECT set_config('request.jwt.claims', '${sql.raw(
stringToken,
)}', TRUE)`;
const query2 = sql`SELECT set_config('request.jwt.claims', '${stringToken}', TRUE)`;
console.log(pgDialect.sqlToQuery(query1));
console.log(pgDialect.sqlToQuery(query2)); Results: // Query 1
{
sql: `SELECT set_config('request.jwt.claims', '{"id":"1"}', TRUE)`,
params: []
}
// Query 2
{
sql: "SELECT set_config('request.jwt.claims', '$1', TRUE)",
params: [ '{"id":"1"}' ]
}
Extending type PgTransactionOptions = {
isolationLevel?: 'read uncommitted' | 'read committed' | 'repeatable read' | 'serializable';
accessMode?: 'read only' | 'read write';
deferrable?: boolean;
configs?: { name: string, value:string, isLocal: boolean }[];
role?: string;
} Configs and role are not only related to RLS or Supabase. 2 possibilities here:
That could be cool! |
Beta Was this translation helpful? Give feedback.
-
I would love the option to have a db query client that automatically applies a transaction for each query, similar to Prisma's client extensions. const queryClient = postgres(process.env.DATABASE_URL)
export const db = drizzle(queryClient, {
schema,
forceTransactions: true, // always use transactions for this db
beginTransaction: (tx) => tx.execute(sql`SET LOCAL app.user_id = "${sql.raw(parsedUserId)}"` // function to be called at the beginning of the transaction (optional)
endTransaction: undefined, // function to be called at the end of the transaction (optional)
}) This would be a light convenient helper that conforms to standard postgres instead of using 3rd party (supabase) apis. |
Beta Was this translation helpful? Give feedback.
-
It looks good to me. Rollback should work, but if you want to be sure you can test by throwing after the last execute and you should see that nothing happens. |
Beta Was this translation helpful? Give feedback.
-
@rphlmr that's nice to know, so far this seems the most logical rls implementation with tRPC, so far the |
Beta Was this translation helpful? Give feedback.
-
Any update???? 🥲 |
Beta Was this translation helpful? Give feedback.
-
Up Up Up any update :/ |
Beta Was this translation helpful? Give feedback.
-
What's working really well for me is the following. import postgres from "postgres";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/postgres-js";
import * as schema from "./drizzle/schema.ts";
import { JwtPayload } from "jwt-decode";
const connectionString = Deno.env.get("DB_URL")!;
export const getDb = (decodedJwt: JwtPayload) => {
// Disable prefetch as it is not supported for "Transaction" pool mode
const client = postgres(connectionString, { prepare: false });
const db = drizzle(client, { schema });
return new Proxy<typeof db>(db, {
get(target, prop) {
if (prop === "transaction") {
return (async (transaction, ...rest) => {
return await target.transaction(async (tx) => {
// Emulates RLS
// https://github.com/drizzle-team/drizzle-orm/issues/594
await tx.execute(sql`
select set_config('request.jwt.claims', '${sql.raw(
JSON.stringify(decodedJwt)
)}', TRUE);
select set_config('request.jwt.claim.sub', '${sql.raw(
decodedJwt.sub ?? ""
)}', TRUE);
select set_config('request.jwt.claim.email', '${sql.raw(
decodedJwt.email
)}', TRUE);
select set_config('request.jwt.claim.role', '${sql.raw(
decodedJwt.role
)}', TRUE);
set local role ${sql.raw(decodedJwt.role)};
`);
return await transaction(tx);
}, ...rest);
}) as typeof db.transaction;
}
},
});
}; I took inspiration from this thread and modified it to fit my needs. I batched all of the Example # Intentionally the same role as used in PostgREST
# This enforces RLS to be respected
DB_URL="postgresql://authenticator:postgres@db:5432/postgres" |
Beta Was this translation helpful? Give feedback.
-
@RyanClementsHax hasn't this introduced latency for you? |
Beta Was this translation helpful? Give feedback.
-
This likely isn't drizzle latency but due to poor performance from RLS
policies.
…On Tue, May 14, 2024, 13:56 Sanyam Jain ***@***.***> wrote:
@RyanClementsHax <https://github.com/RyanClementsHax> hasn't this
introduced latency for you?
I am seeing the same query take 1.2s now which took 200ms before.
—
Reply to this email directly, view it on GitHub
<#594 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAFL6NHFPXVVGR25GAFHM7DZCJF3HAVCNFSM6AAAAAAYGNJ5CWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCMJQHAYDMNRRHE>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
I haven't noticed it, but I have yet to deploy it in prod. |
Beta Was this translation helpful? Give feedback.
-
i want this bad. |
Beta Was this translation helpful? Give feedback.
-
Dropping in to say that implementing this in userland is fine by me, I'm quite happy with the solution using transactions and haven't had any blockers with this yet. I'm not entirely sure if/how drizzle should add support in the library. |
Beta Was this translation helpful? Give feedback.
-
Can you share how exactly this is done? I can't find any resources online. |
Beta Was this translation helpful? Give feedback.
-
This only affects your website that is using Drizzle correct? Even though RLS doesn't work with Drizzle it is still working to protect the database from other people correct? |
Beta Was this translation helpful? Give feedback.
-
Easiest way I've found is to just add some raw sql in import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { sql } from 'drizzle-orm'
import { db, client } from './db';
// This will run migrations on the database, skipping the ones already applied
await migrate(db, { migrationsFolder: './db/migrations' });
// Run SQL to enable RLS on any newly created tables
const enableRls = sql`
DO $$
DECLARE
rec RECORD;
BEGIN
-- Loop through each table without RLS enabled
FOR rec IN
SELECT schemaname, tablename
FROM pg_tables
WHERE schemaname IN ('public')
AND tablename NOT IN (
SELECT c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relrowsecurity = true
)
LOOP
-- Enable RLS on the table
EXECUTE 'ALTER TABLE ' || quote_ident(rec.schemaname) || '.' || quote_ident(rec.tablename) || ' ENABLE ROW LEVEL SECURITY';
END LOOP;
END $$;
`;
await db.execute(enableRls);
// Don't forget to close the connection, otherwise the script will hang
await client.end(); |
Beta Was this translation helpful? Give feedback.
-
Update: finishing pull and push logic for policies and roles and preparing for beta release |
Beta Was this translation helpful? Give feedback.
-
Describe want to want
Supabase is really nicely using Row Level Secruity for granular authorization rules.
🔗 Here's the link to their docs: https://supabase.com/docs/guides/auth/row-level-security
I'd love to switch from Supabase JS SDK to drizzle based on all the features, but one limitation right now is that it seems it does not support row level secruity.
Would love if you would consider adding this feature if also other users would find it helpful!
Beta Was this translation helpful? Give feedback.
All reactions