테이블에 정책을 추가하면 RLS가 자동으로 활성화됩니다. 따라서 테이블에 정책을 추가할 때 RLS를 명시적으로 활성화할 필요가 없습니다.
행 단위 보안(Row-Level Security, RLS)
Drizzle를 사용하면 Postgres 테이블에 대해 **행 단위 보안(RLS)**을 활성화할 수 있습니다. 다양한 옵션으로 정책을 생성하고, 해당 정책이 적용될 역할을 정의하고 관리할 수 있습니다.
Drizzle는 Postgres 정책과 역할을 원시(raw) 형태로 지원하며, 이를 원하는 방식으로 활용할 수 있습니다. 이 기능은 Neon
이나 Supabase
와 같은 인기 있는 Postgres 데이터베이스 프로바이더와 함께 작동합니다.
Drizzle에서는 두 데이터베이스 프로바이더를 위한 특정 사전 정의된 RLS 역할과 함수를 제공합니다. 하지만 여러분이 직접 로직을 정의할 수도 있습니다.
RLS 활성화
테이블에 정책을 추가하지 않고 RLS(Row Level Security)만 활성화하려면 .enableRLS()
를 사용할 수 있습니다.
PostgreSQL 문서에 언급된 대로:
테이블에 정책이 존재하지 않으면 기본적으로 거부 정책이 적용됩니다. 이는 어떤 행도 보이지 않거나 수정할 수 없음을 의미합니다. TRUNCATE 및 REFERENCES와 같은 전체 테이블에 적용되는 작업은 행 수준 보안의 영향을 받지 않습니다.
import { integer, pgTable } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: integer(),
}).enableRLS();
역할(Roles)
현재 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 테이블 내에서 정책을 정의할 수 있습니다.
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-kit이 각 역할을 스키마에 정의하고 .existing()
으로 표시하지 않고도 해당 역할을 관리하지 않도록 할 수 있습니다.
이런 경우 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'
}
}
});
데이터베이스 프로바이더가 지정한 새로운 역할에 비해 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))
);
이 예제에서는 rooms_users_profiles
라는 뷰를 생성하면서 securityInvoker
옵션을 활성화했습니다. 이렇게 하면 뷰에 접근할 때 RLS 정책이 적용됩니다.
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
와 anonymous
역할 및 관련 함수를 제공합니다. 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})`;
예를 들어, 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와 함께 사용하기
Drizzle은 Supabase와 함께 사용할 수 있도록 미리 정의된 역할들을 포함한 /supabase
임포트를 제공합니다. 이 임포트는 향후 릴리스에서 RLS(Row Level Security)와 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 저장소를 확인해 보세요.
이 저장소에서 구현된 예제는 다음과 같습니다:
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; client: PgDatabase }) {
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));
}