如果您向表添加策略,RLS 将自动启用。因此,在向表添加策略时,无需显式启用 RLS。
行级安全 (RLS)
通过 Drizzle,您可以为任何 Postgres 表启用行级安全 (RLS),创建具有各种选项的策略,并定义和管理这些策略适用的角色。
Drizzle 支持 Postgres 策略和角色的原始表示,您可以根据需要以任何方式使用它们。它适用于流行的 Postgres 数据库提供商,如 Neon
和 Supabase
。
在 Drizzle 中,我们为这两种数据库提供商的 RLS 提供了特定的预定义 RLS 角色和函数,但您也可以定义自己的逻辑。
启用 RLS
如果您只想在表中启用 RLS 而不添加策略,可以使用 .enableRLS()
正如 PostgreSQL 文档中所述
如果表没有策略,则使用默认拒绝策略,这意味着任何行都不可见或无法修改。适用于整个表的操作,例如 TRUNCATE 和 REFERENCES,不受行安全限制。
import { integer, pgTable } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: integer(),
}).enableRLS();
角色
目前,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 管理您的 schema 和角色,可能会遇到需要引用未在 Drizzle schema 中定义的角色的情况。在这种情况下,您可能希望 drizzle-kit 跳过管理这些角色,而无需在 Drizzle schema 中定义每个角色并将其标记为 .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))
);
与 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
(匿名)角色以及相关函数。如果您将 Neon
用于 RLS,则可以在 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
导入,其中包含一组标记为预定义的现有角色,您可以在 schema 中使用它们。此导入将在未来版本中扩展,增加更多函数和助手,以简化 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],
// reference to the auth table from Supabase
foreignColumns: [authUsers.id],
name: "profiles_id_fk",
}).onDelete("cascade"),
pgPolicy("authenticated can view all profiles", {
for: "select",
// using predefined role from 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 结合使用以及如何进行实际查询。它还包含一个出色的包装器 createDrizzle
,可以为您处理与 Supabase 的所有事务性工作。在即将发布的版本中,它将被移至 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<any>; client: PgDatabase<any> }) {
return {
admin,
rls: (async (transaction, ...rest) => {
return await client.transaction(async (tx) => {
// Supabase exposes auth.uid() and 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
// Should be secure because we use the access token that is signed, and not the data read directly from the storage
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));
}