Row-Level Security (RLS)

Drizzle๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด Postgres ํ…Œ์ด๋ธ”์— ๋Œ€ํ•ด RLS(ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ)๋ฅผ ํ™œ์„ฑํ™”ํ•˜๊ณ , ๋‹ค์–‘ํ•œ ์˜ต์…˜์œผ๋กœ ์ •์ฑ…์„ ์ƒ์„ฑํ•˜๋ฉฐ, ํ•ด๋‹น ์ •์ฑ…์ด ์ ์šฉ๋˜๋Š” ์—ญํ• ์„ ์ •์˜ํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Drizzle์€ ์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” Postgres ์ •์ฑ… ๋ฐ ์—ญํ• ์˜ ์›์‹œ ํ‘œํ˜„์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ์ด๋Š” Neon ๋ฐ Supabase์™€ ๊ฐ™์€ ์ธ๊ธฐ ์žˆ๋Š” Postgres ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ œ๊ณต์—…์ฒด์—์„œ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

Drizzle์—๋Š” ๋‘ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ œ๊ณต์—…์ฒด ๋ชจ๋‘์— ๋Œ€ํ•ด ๋ฏธ๋ฆฌ ์ •์˜๋œ RLS ์—ญํ•  ๋ฐ ํ•จ์ˆ˜๊ฐ€ ์žˆ์ง€๋งŒ, ๊ณ ์œ ํ•œ ๋กœ์ง์„ ์ •์˜ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

RLS ํ™œ์„ฑํ™”

์ •์ฑ…์„ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๊ณ  ํ…Œ์ด๋ธ”์—์„œ RLS๋งŒ ํ™œ์„ฑํ™”ํ•˜๋ ค๋ฉด .enableRLS()๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

PostgreSQL ๋ฌธ์„œ์— ์–ธ๊ธ‰๋œ ๋Œ€๋กœ:

ํ…Œ์ด๋ธ”์— ์ •์ฑ…์ด ์—†๋Š” ๊ฒฝ์šฐ ๊ธฐ๋ณธ ๊ฑฐ๋ถ€ ์ •์ฑ…์ด ์‚ฌ์šฉ๋˜๋ฏ€๋กœ ํ–‰์ด ๋ณด์ด์ง€ ์•Š๊ฑฐ๋‚˜ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. TRUNCATE ๋ฐ REFERENCES์™€ ๊ฐ™์ด ์ „์ฒด ํ…Œ์ด๋ธ”์— ์ ์šฉ๋˜๋Š” ์ž‘์—…์€ ํ–‰ ๋ณด์•ˆ์˜ ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

import { integer, pgTable } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
	id: integer(),
}).enableRLS();
important

ํ…Œ์ด๋ธ”์— ์ •์ฑ…์„ ์ถ”๊ฐ€ํ•˜๋ฉด RLS๊ฐ€ ์ž๋™์œผ๋กœ ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ…Œ์ด๋ธ”์— ์ •์ฑ…์„ ์ถ”๊ฐ€ํ•  ๋•Œ ๋ช…์‹œ์ ์œผ๋กœ RLS๋ฅผ ํ™œ์„ฑํ™”ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์—ญํ• 

ํ˜„์žฌ Drizzle์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ช‡ ๊ฐ€์ง€ ๋‹ค๋ฅธ ์˜ต์…˜์œผ๋กœ ์—ญํ•  ์ •์˜๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. ํ–ฅํ›„ ๋ฆด๋ฆฌ์Šค์—์„œ ๋” ๋งŽ์€ ์˜ต์…˜์— ๋Œ€ํ•œ ์ง€์›์ด ์ถ”๊ฐ€๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

import { pgRole } from 'drizzle-orm/pg-core';

export const admin = pgRole('admin', { createRole: true, createDb: true, inherit: true });

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ญํ• ์ด ์ด๋ฏธ ์กด์žฌํ•˜๊ณ  drizzle-kit๊ฐ€ ์ด๋ฅผ โ€˜ํ™•์ธโ€™ํ•˜๊ฑฐ๋‚˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์— ํฌํ•จํ•˜์ง€ ์•Š๋„๋ก ํ•˜๋ ค๋ฉด ์—ญํ• ์„ ๊ธฐ์กด ๊ฒƒ์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { pgRole } from 'drizzle-orm/pg-core';

export const admin = pgRole('admin').existing();

์ •์ฑ…

RLS๋ฅผ ์™„์ „ํžˆ ํ™œ์šฉํ•˜๋ ค๋ฉด Drizzle ํ…Œ์ด๋ธ” ๋‚ด์—์„œ ์ •์ฑ…์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

info

PostgreSQL์—์„œ ์ •์ฑ…์€ ๊ธฐ์กด ํ…Œ์ด๋ธ”์— ์—ฐ๊ฒฐ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ •์ฑ…์€ ํ•ญ์ƒ ํŠน์ • ํ…Œ์ด๋ธ”๊ณผ ์—ฐ๊ฒฐ๋˜๋ฏ€๋กœ ์ •์ฑ… ์ •์˜๋Š” pgTable์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ •์˜๋˜์–ด์•ผ ํ•œ๋‹ค๊ณ  ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์†์„ฑ์ด ํฌํ•จ๋œ pgPolicy ์˜ˆ์ œ

import { sql } from 'drizzle-orm';
import { integer, pgPolicy, pgRole, pgTable } from 'drizzle-orm/pg-core';

export const admin = pgRole('admin');

export const users = pgTable('users', {
	id: integer(),
}, (t) => [
	pgPolicy('policy', {
		as: 'permissive',
		to: admin,
		for: 'delete',
		using: sql``,
		withCheck: sql``,
	}),
]);

์ •์ฑ… ์˜ต์…˜

as๊ฐ€๋Šฅํ•œ ๊ฐ’์€ permissive ๋˜๋Š” restrictive์ž…๋‹ˆ๋‹ค.
to์ •์ฑ…์ด ์ ์šฉ๋˜๋Š” ์—ญํ• ์„ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•œ ๊ฐ’์—๋Š” public, current_role, current_user, session_user ๋˜๋Š” ๋ฌธ์ž์—ด๋กœ ๋œ ๋‹ค๋ฅธ ์—ญํ•  ์ด๋ฆ„์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. pgRole ๊ฐ์ฒด๋ฅผ ์ฐธ์กฐํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
for์ด ์ •์ฑ…์ด ์ ์šฉ๋  ๋ช…๋ น์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋Šฅํ•œ ๊ฐ’์€ all, select, insert, update, delete์ž…๋‹ˆ๋‹ค.
using์ •์ฑ… ์ƒ์„ฑ ๋ฌธ์˜ USING ๋ถ€๋ถ„์— ์ ์šฉ๋  SQL ๋ฌธ์ž…๋‹ˆ๋‹ค.
withCheck์ •์ฑ… ์ƒ์„ฑ ๋ฌธ์˜ WITH CHECK ๋ถ€๋ถ„์— ์ ์šฉ๋  SQL ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ธฐ์กด ํ…Œ์ด๋ธ”์— ์ •์ฑ… ์—ฐ๊ฒฐ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๊ธฐ์กด ํ…Œ์ด๋ธ”์— ์ •์ฑ…์„ ์—ฐ๊ฒฐํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€๋Š” Neon ๋˜๋Š” Supabase์™€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ œ๊ณต์—…์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ๋กœ, ๊ธฐ์กด ํ…Œ์ด๋ธ”์— ์ •์ฑ…์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ .link() API๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { sql } from "drizzle-orm";
import { pgPolicy } from "drizzle-orm/pg-core";
import { authenticatedRole, realtimeMessages } from "drizzle-orm/supabase";

export const policy = pgPolicy("authenticated role insert policy", {
  for: "insert",
  to: authenticatedRole,
  using: sql``,
}).link(realtimeMessages);

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

drizzle-kit๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํ‚ค๋งˆ์™€ ์—ญํ• ์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ Drizzle ์Šคํ‚ค๋งˆ์— ์ •์˜๋˜์ง€ ์•Š์€ ์—ญํ• ์„ ์ฐธ์กฐํ•˜๋ ค๋Š” ์ƒํ™ฉ์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ drizzle ์Šคํ‚ค๋งˆ์— ๊ฐ ์—ญํ• ์„ ์ •์˜ํ•˜๊ณ  .existing()์œผ๋กœ ํ‘œ์‹œํ•˜์ง€ ์•Š๊ณ ๋„ drizzle-kit๊ฐ€ ์ด๋Ÿฌํ•œ ์—ญํ•  ๊ด€๋ฆฌ๋ฅผ ๊ฑด๋„ˆ๋›ฐ๋„๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ drizzle.config.ts์—์„œ entities.roles๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ „์ฒด ์ฐธ์กฐ๋Š” drizzle.config.ts ๋ฌธ์„œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

๊ธฐ๋ณธ์ ์œผ๋กœ drizzle-kit๋Š” ์—ญํ• ์„ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ drizzle.config.ts์—์„œ ์ด ๊ธฐ๋Šฅ์„ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: 'postgresql',
  schema: "./drizzle/schema.ts",
  dbCredentials: {
    url: process.env.DATABASE_URL!
  },
  verbose: true,
  strict: true,
  entities: {
    roles: true
  }
});

์ถ”๊ฐ€ ๊ตฌ์„ฑ ์˜ต์…˜์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๋ช‡ ๊ฐ€์ง€ ์˜ˆ์ œ๋ฅผ ๋” ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

admin ์—ญํ• ์ด ์žˆ๊ณ  ๊ด€๋ฆฌ ๊ฐ€๋Šฅํ•œ ์—ญํ•  ๋ชฉ๋ก์—์„œ ์ œ์™ธํ•˜๋ ค๋Š” ๊ฒฝ์šฐ

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  ...
  entities: {
    roles: {
      exclude: ['admin']
    }
  }
});

admin ์—ญํ• ์ด ์žˆ๊ณ  ๊ด€๋ฆฌ ๊ฐ€๋Šฅํ•œ ์—ญํ•  ๋ชฉ๋ก์— ํฌํ•จํ•˜๋ ค๋Š” ๊ฒฝ์šฐ

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  ...
  entities: {
    roles: {
      include: ['admin']
    }
  }
});

Neon์„ ์‚ฌ์šฉํ•˜๊ณ  Neon์—์„œ ์ •์˜ํ•œ ์—ญํ• ์„ ์ œ์™ธํ•˜๋ ค๋Š” ๊ฒฝ์šฐ provider ์˜ต์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  ...
  entities: {
    roles: {
      provider: 'neon'
    }
  }
});

Supabase๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  Supabase์—์„œ ์ •์˜ํ•œ ์—ญํ• ์„ ์ œ์™ธํ•˜๋ ค๋Š” ๊ฒฝ์šฐ provider ์˜ต์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  ...
  entities: {
    roles: {
      provider: 'supabase'
    }
  }
});
important

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ œ๊ณต์—…์ฒด์—์„œ ์ง€์ •ํ•œ ์ƒˆ ์—ญํ• ๊ณผ ๋น„๊ตํ•˜์—ฌ Drizzle์ด ์•ฝ๊ฐ„ ์˜ค๋ž˜๋œ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ provider ์˜ต์…˜์„ ์‚ฌ์šฉํ•˜๊ณ  ์ถ”๊ฐ€ ์—ญํ• ์„ excludeํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  ...
  entities: {
    roles: {
      provider: 'supabase',
      exclude: ['new_supabase_role']
    }
  }
});

๋ทฐ์˜ RLS

Drizzle๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ทฐ์— ๋Œ€ํ•œ RLS ์ •์ฑ…๋„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” ๋ทฐ์˜ WITH ์˜ต์…˜์—์„œ security_invoker๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค:

...

export const roomsUsersProfiles = pgView("rooms_users_profiles")
  .with({
    securityInvoker: true,
  })
  .as((qb) =>
    qb
      .select({
        ...getTableColumns(roomsUsers),
        email: profiles.email,
      })
      .from(roomsUsers)
      .innerJoin(profiles, eq(roomsUsers.userId, profiles.id))
  );

Neon๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ

Neon ํŒ€์€ ์›์‹œ ์ •์ฑ… API ์œ„์— ๋ž˜ํผ์— ๋Œ€ํ•œ ๋น„์ „์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ๋„์›€์„ ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฏธ๋ฆฌ ์ •์˜๋œ ํ•จ์ˆ˜์™€ Neon์˜ ๊ธฐ๋ณธ ์—ญํ• ์„ ํฌํ•จํ•˜๋Š” crudPolicy ํ•จ์ˆ˜๊ฐ€ ์žˆ๋Š” ํŠน์ • /neon ๊ฐ€์ ธ์˜ค๊ธฐ๋ฅผ ์ •์˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

crudPolicy ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์˜ ์˜ˆ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

import { crudPolicy } from 'drizzle-orm/neon';
import { integer, pgRole, pgTable } from 'drizzle-orm/pg-core';

export const admin = pgRole('admin');

export const users = pgTable('users', {
	id: integer(),
}, (t) => [
	crudPolicy({ role: admin, read: true, modify: false }),
]);

์ด ์ •์ฑ…์€ ๋‹ค์Œ๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค:

import { sql } from 'drizzle-orm';
import { integer, pgPolicy, pgRole, pgTable } from 'drizzle-orm/pg-core';

export const admin = pgRole('admin');

export const users = pgTable('users', {
	id: integer(),
}, (t) => [
	pgPolicy(`crud-${admin.name}-policy-insert`, {
		for: 'insert',
		to: admin,
		withCheck: sql`false`,
	}),
	pgPolicy(`crud-${admin.name}-policy-update`, {
		for: 'update',
		to: admin,
		using: sql`false`,
		withCheck: sql`false`,
	}),
	pgPolicy(`crud-${admin.name}-policy-delete`, {
		for: 'delete',
		to: admin,
		using: sql`false`,
	}),
	pgPolicy(`crud-${admin.name}-policy-select`, {
		for: 'select',
		to: admin,
		using: sql`true`,
	}),
]);

Neon์€ ๋ฏธ๋ฆฌ ์ •์˜๋œ authenticated ๋ฐ anaonymous ์—ญํ• ๊ณผ ๊ด€๋ จ ํ•จ์ˆ˜๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค. RLS์— Neon์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๊ธฐ์กด์œผ๋กœ ํ‘œ์‹œ๋œ ์ด๋Ÿฌํ•œ ์—ญํ• ๊ณผ RLS ์ฟผ๋ฆฌ์˜ ๊ด€๋ จ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// drizzle-orm/neon
export const authenticatedRole = pgRole('authenticated').existing();
export const anonymousRole = pgRole('anonymous').existing();

export const authUid = (userIdColumn: AnyPgColumn) => sql`(select auth.user_id() = ${userIdColumn})`;

export const neonIdentitySchema = pgSchema('neon_identity');

export const usersSync = neonIdentitySchema.table('users_sync', {
  rawJson: jsonb('raw_json').notNull(),
  id: text().primaryKey().notNull(),
  name: text(),
  email: text(),
  createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }),
  deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }),
});

์˜ˆ๋ฅผ ๋“ค์–ด Neon ๋ฏธ๋ฆฌ ์ •์˜๋œ ์—ญํ•  ๋ฐ ํ•จ์ˆ˜๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

import { sql } from 'drizzle-orm';
import { authenticatedRole } from 'drizzle-orm/neon';
import { integer, pgPolicy, pgRole, pgTable } from 'drizzle-orm/pg-core';

export const admin = pgRole('admin');

export const users = pgTable('users', {
	id: integer(),
}, (t) => [
	pgPolicy(`policy-insert`, {
		for: 'insert',
		to: authenticatedRole,
		withCheck: sql`false`,
	}),
]);

Supabase์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ

๋˜ํ•œ ์Šคํ‚ค๋งˆ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ์กด์œผ๋กœ ํ‘œ์‹œ๋œ ๋ฏธ๋ฆฌ ์ •์˜๋œ ์—ญํ•  ์„ธํŠธ๊ฐ€ ์žˆ๋Š” /supabase ๊ฐ€์ ธ์˜ค๊ธฐ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ€์ ธ์˜ค๊ธฐ๋Š” ํ–ฅํ›„ ๋ฆด๋ฆฌ์Šค์—์„œ RLS ๋ฐ Supabase ์‚ฌ์šฉ์„ ๋” ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๋” ๋งŽ์€ ํ•จ์ˆ˜์™€ ํ—ฌํผ๋กœ ํ™•์žฅ๋  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

// drizzle-orm/supabase
export const anonRole = pgRole('anon').existing();
export const authenticatedRole = pgRole('authenticated').existing();
export const serviceRole = pgRole('service_role').existing();
export const postgresRole = pgRole('postgres_role').existing();
export const supabaseAuthAdminRole = pgRole('supabase_auth_admin').existing();

์˜ˆ๋ฅผ ๋“ค์–ด Supabase ๋ฏธ๋ฆฌ ์ •์˜๋œ ์—ญํ• ์„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

import { sql } from 'drizzle-orm';
import { serviceRole } from 'drizzle-orm/supabase';
import { integer, pgPolicy, pgRole, pgTable } from 'drizzle-orm/pg-core';

export const admin = pgRole('admin');

export const users = pgTable('users', {
	id: integer(),
}, (t) => [
	pgPolicy(`policy-insert`, {
		for: 'insert',
		to: serviceRole,
		withCheck: sql`false`,
	}),
]);

/supabase ๊ฐ€์ ธ์˜ค๊ธฐ์—๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฏธ๋ฆฌ ์ •์˜๋œ ํ…Œ์ด๋ธ” ๋ฐ ํ•จ์ˆ˜๋„ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

// drizzle-orm/supabase

const auth = pgSchema('auth');
export const authUsers = auth.table('users', {
	id: uuid().primaryKey().notNull(),
});

const realtime = pgSchema('realtime');
export const realtimeMessages = realtime.table(
	'messages',
	{
		id: bigserial({ mode: 'bigint' }).primaryKey(),
		topic: text().notNull(),
		extension: text({
			enum: ['presence', 'broadcast', 'postgres_changes'],
		}).notNull(),
	},
);

export const authUid = sql`(select auth.uid())`;
export const realtimeTopic = sql`realtime.topic()`;

์ด๋ฅผ ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ Drizzle Kit๋Š” ์ด๋ฅผ ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ์ทจ๊ธ‰ํ•˜์—ฌ ๋‹ค๋ฅธ ์—”ํ„ฐํ‹ฐ์— ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ์ •๋ณด๋กœ๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

import { foreignKey, pgPolicy, pgTable, text, uuid } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm/sql";
import { authenticatedRole, authUsers } from "drizzle-orm/supabase";

export const profiles = pgTable(
  "profiles",
  {
    id: uuid().primaryKey().notNull(),
    email: text().notNull(),
  },
  (table) => [
    foreignKey({
      columns: [table.id],
	  // Supabase์˜ auth ํ…Œ์ด๋ธ” ์ฐธ์กฐ
      foreignColumns: [authUsers.id],
      name: "profiles_id_fk",
    }).onDelete("cascade"),
    pgPolicy("authenticated can view all profiles", {
      for: "select",
	  // Supabase์˜ ๋ฏธ๋ฆฌ ์ •์˜๋œ ์—ญํ•  ์‚ฌ์šฉ
      to: authenticatedRole,
      using: sql`true`,
    }),
  ]
);

Supabase์— ์กด์žฌํ•˜๋Š” ํ…Œ์ด๋ธ”์— ์ •์ฑ…์„ ์ถ”๊ฐ€ํ•˜๋Š” ์˜ˆ์ œ๋ฅผ ํ™•์ธํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

import { sql } from "drizzle-orm";
import { pgPolicy } from "drizzle-orm/pg-core";
import { authenticatedRole, realtimeMessages } from "drizzle-orm/supabase";

export const policy = pgPolicy("authenticated role insert policy", {
  for: "insert",
  to: authenticatedRole,
  using: sql``,
}).link(realtimeMessages);

๋˜ํ•œ Drizzle RLS๋ฅผ Supabase์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ์‹ค์ œ ์ฟผ๋ฆฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ฃผ๋Š” ํ›Œ๋ฅญํ•œ ์˜ˆ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ Supabase์™€์˜ ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ›Œ๋ฅญํ•œ ๋ž˜ํผ์ธ createDrizzle๋„ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ–ฅํ›„ ๋ฆด๋ฆฌ์Šค์—์„œ๋Š” drizzle-orm/supabase๋กœ ์ด๋™๋˜์–ด ๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

Drizzle SupaSecureSlack repo๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

๋‹ค์Œ์€ ์ด ์ €์žฅ์†Œ์˜ ๊ตฌํ˜„ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.

type SupabaseToken = {
  iss?: string;
  sub?: string;
  aud?: string[] | string;
  exp?: number;
  nbf?: number;
  iat?: number;
  jti?: string;
  role?: string;
};

export function createDrizzle(token: SupabaseToken, { admin, client }: { admin: PgDatabase<any>; client: PgDatabase<any> }) {
  return {
    admin,
    rls: (async (transaction, ...rest) => {
      return await client.transaction(async (tx) => {
        // Supabase๋Š” auth.uid() ๋ฐ auth.jwt()๋ฅผ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค
        // https://supabase.com/docs/guides/database/postgres/row-level-security#helper-functions
        try {
          await tx.execute(sql`
          -- auth.jwt()
          select set_config('request.jwt.claims', '${sql.raw(
            JSON.stringify(token)
          )}', TRUE);
          -- auth.uid()
          select set_config('request.jwt.claim.sub', '${sql.raw(
            token.sub ?? ""
          )}', TRUE);
          -- set local role
          set local role ${sql.raw(token.role ?? "anon")};
          `);
          return await transaction(tx);
        } finally {
          await tx.execute(sql`
            -- reset
            select set_config('request.jwt.claims', NULL, TRUE);
            select set_config('request.jwt.claim.sub', NULL, TRUE);
            reset role;
            `);
        }
      }, ...rest);
    }) as typeof client.transaction,
  };
}

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// https://github.com/orgs/supabase/discussions/23224
// ์„œ๋ช…๋œ ์•ก์„ธ์Šค ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๊ณ  ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ง์ ‘ ์ฝ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์•ˆ์ „ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
export async function createDrizzleSupabaseClient() {
  const {
    data: { session },
  } = await createClient().auth.getSession();
  return createDrizzle(decode(session?.access_token ?? ""), { admin, client });
}

async function getRooms() {
  const db = await createDrizzleSupabaseClient();
  return db.rls((tx) => tx.select().from(rooms));
}