【问题标题】:pg-promise and Row Level Securitypg-promise 和行级安全性
【发布时间】:2021-02-04 04:56:09
【问题描述】:

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

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

  1. 创建一个 getDb(tenantId) 包装器,它在返回 db 对象之前调用 SET app.current_tenant = '${tenantId}';` sql 语句
  2. getDb(tenantId) 包装器,每次都获取一个新的 db 对象 - 这适用于一些请求,但最终会导致过多的 db 连接和错误输出(这是可以理解的,因为它没有使用 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;

【问题讨论】:

  • 啊,这么多的请求,我刚刚开始向xcode 移动,在这些困难时期尝试从中赚取至少一些钱。但我会尽量抽出时间在这里回答:)如果很紧急,您可以尝试GitHub项目页面上的sponsor按钮。我刚刚设置,还没用,所以你会是第一个:)
  • 我的回答没有帮助吗?如果是,请接受。
  • 嘿,vitally-t。太感谢了。很抱歉没有早点回应 - 一直忙于实施变更。您的回复证实了我们使用任务/txs 的方法。而且,是的,我会把我的老板引向赞助商按钮。我们非常感谢您的出色工作。保持安全和健康的伴侣

标签: pg-promise row-level-security


【解决方案1】:

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 库都应该非常简单,但超出了这里的范围。

【讨论】:

  • 谢谢活力-t - 确认了我们的 2 种方法。谢谢
猜你喜欢
  • 2011-06-29
  • 1970-01-01
  • 2021-04-10
  • 2019-05-10
  • 2022-07-14
  • 2017-11-29
  • 2018-09-23
  • 2018-08-15
  • 2018-10-12
相关资源
最近更新 更多