pg-promise 和行级安全性

问题描述 投票:0回答:1

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

我们尝试了一些方法但没有成功:

  1. 创建一个 getDb(tenantId) 包装器,在返回 db 对象之前调用 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;

pg-promise row-level-security
1个回答
2
投票

SET
操作是连接绑定的,即该操作仅在当前连接会话持续时有效。对于池生成的新连接,您需要重新应用设置。

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

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 = $1', [tenantId]);
    }
};

后者是一种事后考虑的解决方法,但它确实工作可靠,具有最佳性能,并且避免创建额外的任务。

尚未找到获取当前请求对象的方法(然后设置tenant_id)

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

© www.soinside.com 2019 - 2024. All rights reserved.