pg-promise 和行级安全

pg-promise and Row Level Security

我正在考虑使用我们的 node express + pg-promise + postgres 服务实现行级安全性。

我们尝试了几种方法都没有成功:

  1. 创建一个 getDb(tenantId) 包装器,它在返回数据库对象之前调用 SET app.current_tenant = '${tenantId}';` sql 语句
  2. getDb(tenantId) 每次获取一个新的数据库对象的包装器——这适用于一些请求,但最终会导致太多的数据库连接和错误(这是可以理解的,因为它没有使用 pg-promise 的连接池管理)
  3. getDb(tenantId) 包装器,它使用名称值(映射)来存储每个租户的数据库连接列表。这工作了一小段时间,但最终导致数据库连接过多。
  4. 利用 initOptions > connect 事件 - 尚未找到获取当前请求对象的方法(然后设置 tenant_id)

有人(希望是 vitaly-t :))可以建议在所有 sql 查询都 运行 连接之前注入当前租户的最佳策略。

非常感谢

这里是一个简短的代码示例:

const promise = require('bluebird');

const initOptions = {
  promiseLib: promise,
  connect: async (client, dc, useCount) => {
    try {
      // "hook" into the db connect event - and set the tenantId so all future sql queries in this connection 
      // have an implied WHERE tenant_id = app.current_setting('app.current_tenant')::UUID   (aka PostGres Row Level Security)
      const tenantId = client.$ctx?.cn?.tenantId || client.$ctx?.cnOptions?.tenantId;
      if (tenantId) {
        await client.query(`SET app.current_tenant = '${tenantId}';`);
      }
    } catch (ex) {
      log.error('error in db.js initOptions', {ex});
    }
  }
};
const pgp = require('pg-promise')(initOptions);

const options = tenantIdOptional => {
  return {
    user: process.env.POSTGRES_USER,
    host: process.env.POSTGRES_HOST,
    database: process.env.POSTGRES_DATABASE, 
    password: process.env.POSTGRES_PASSWORD,
    port: process.env.POSTGRES_PORT,
    max: 100,
    tenantId: tenantIdOptional
  };
};

const db = pgp(options());
const getDb = tenantId => {
  // how to inject tenantId into the db object
  // 1. this was getting an error "WARNING: Creating a duplicate database object for the same connection  and  Error: write EPIPE"
  // const tmpDb = pgp(options(tenantId));
  // return tmpDb;

  // 2. this was running the set app.current_tenant BEFORE the database connection was established
  // const setTenantId = async () => {
  //   await db.query(`SET app.current_tenant = '${tenantId}';`);
  // };
  // setTenantId();
  // return db;

  // 3. this is bypassing the connection pool management - and is not working
  // db.connect(options(tenantId));
  // return db;
  return db;
};

// Exporting the global database object for shared use:
const exportFunctions = {
  getDb,
  db        // have to also export db for the legacy non-Row level security areas of the service  
};
module.exports = exportFunctions;

SET操作是connection-bound,即操作仅在当前连接会话持续时有效。对于池生成的新连接,您需要 re-apply 设置。

控制当前连接会话的标准方法是通过任务:

await db.task('my-task', async t => {
    await t.none('SET app.current_tenant = ${tenantId}', {tenantId});

    // ... run all session-related queries here
});

或者,如果需要交易,您可以使用方法 tx

但是如果您 tenantId 全局已知,并且您希望它通过所有连接自动传播,那么您可以改用事件 connect

const initOptions = {
    connect(client) {
        client.query('SET app.current_tenant = ', [tenantId]);
    }
};

后者有点像 after-thought work-around,但它工作可靠,性能最好,并且避免创建额外的任务。

have not found a way to get hold of the current request object (to then set the tenant_id)

这对于现有的任何 HTTP 库来说都应该非常简单,但超出了此处的范围。