行级安全性 (RLS)

使用 Drizzle,你可以为任何 Postgres 表启用行级安全性 (RLS),创建包含各种选项的策略,以及定义和管理这些策略适用的角色。

Drizzle 支持 Postgres 策略和角色的原始表示,你可以根据需要使用它们。这适用于流行的 Postgres 数据库提供程序,例如 NeonSupabase

在 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可能的值为 permissiverestrictive
to指定策略适用的角色。可能的值为 publiccurrent_rolecurrent_usersession_user 或任何其他以字符串形式表示的角色名称。你还可以引用 pgRole 对象。
for定义此策略将应用到的命令。可能的值为 allselectinsertupdatedelete
using将应用于策略创建语句的 USING 部分的 SQL 语句。
withCheck将应用于策略创建语句的 WITH CHECK 部分的 SQL 语句。

将 Policy 链接到现有表

在某些情况下,你需要将策略链接到数据库中的现有表。最常见的用例是与 NeonSupabase 等数据库提供商合作,你需要向其现有表添加策略。在这种情况下,你可以使用 .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 跳过管理这些角色的步骤,而无需在 drizzle 模式中定义每个角色并将其标记为 .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 定义的角色,则可以使用提供程序选项。

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

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

如果你正在使用 Supabase 并希望排除 Supabase 定义的角色,则可以使用提供程序选项。

// 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 之上封装的愿景。我们使用 crudPolicy 函数定义了一个特定的 /neon 导入,该函数包含预定义函数和 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 公开预定义的 authenticatedanaonymous 角色及相关函数。如果你正在使用 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 导入功能,其中包含一组标记为“现有”的预定义角色,你可以在架构中使用它们。此导入功能将在未来的版本中扩展,包含更多函数和助手,以简化 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));
}