pg-promise 和行级安全
pg-promise and Row Level Security
我正在考虑使用我们的 node express + pg-promise + postgres 服务实现行级安全性。
我们尝试了几种方法都没有成功:
- 创建一个 getDb(tenantId) 包装器,它在返回数据库对象之前调用 SET app.current_tenant = '${tenantId}';` sql 语句
- getDb(tenantId) 每次获取一个新的数据库对象的包装器——这适用于一些请求,但最终会导致太多的数据库连接和错误(这是可以理解的,因为它没有使用 pg-promise 的连接池管理)
- getDb(tenantId) 包装器,它使用名称值(映射)来存储每个租户的数据库连接列表。这工作了一小段时间,但最终导致数据库连接过多。
- 利用 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 库来说都应该非常简单,但超出了此处的范围。
我正在考虑使用我们的 node express + pg-promise + postgres 服务实现行级安全性。
我们尝试了几种方法都没有成功:
- 创建一个 getDb(tenantId) 包装器,它在返回数据库对象之前调用 SET app.current_tenant = '${tenantId}';` sql 语句
- getDb(tenantId) 每次获取一个新的数据库对象的包装器——这适用于一些请求,但最终会导致太多的数据库连接和错误(这是可以理解的,因为它没有使用 pg-promise 的连接池管理)
- getDb(tenantId) 包装器,它使用名称值(映射)来存储每个租户的数据库连接列表。这工作了一小段时间,但最终导致数据库连接过多。
- 利用 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 库来说都应该非常简单,但超出了此处的范围。