2

我有一个多租户 Web 应用程序,可能需要支持数十个租户(公司)。我一直在寻找一种方法来确保租户只能访问他们自己的数据(重要的是没有泄漏),而不必传递tenant_id给每个表单和 SQL 查询。我的想法是创建一个可更新的视图,以便用户的查询只能在其公司数据的范围内操作。

我通过创建一个视图(postgres)来做到这一点:

CREATE VIEW products_tenant AS
SELECT *
FROM products
WHERE company_id = cast(current_setting('my.tenant_id') as int)
with local check option;
ALTER VIEW products_tenant ALTER COLUMN company_id SET DEFAULT cast(current_setting('my.tenant_id') as int);

这将创建一个可更新的视图,该视图只允许查询该公司的数据,而无需指定其tenant_id.

在 Diesel 中,我已经编写了表格!视图的宏,因此 Diesel 将它们视为表格。在 Rocket 中,我将我的数据库连接 Request Guard 包装在另一个 Request Guard 中,它首先发送一个 SQL 查询来设置my.tenant_id用户的tenant_id

pub struct TenantView(DbConn);
...

impl<'a, 'r> FromRequest<'a, 'r> for TenantView {
    type Error = ();

    fn from_request(request: &'a Request<'r>) -> Outcome<Self, ()> {
        let conn = request.guard::<DbConn>().unwrap();
        let company_id = request.guard::<User>().unwrap().get_company_id();

        let query = sql_query(format!("SET session my.tenant_id = {}", company_id));
        query.execute(&*conn).expect("Failed to set session variable");
        Outcome::Success(Self(conn))
    }
}

然后,用户可以使用请求保护进行数据库查询,并且只能访问他们公司的数据。租户特定的可更新视图。

但我担心这可能会导致竞争状况。我不清楚 postgres 会话变量如何与 Diesel 和 Rocket 一起使用。假设来自两个不同公司的用户同时向 Rocket 提交请求,并且用户 A 的会话变量设置为他们的租户 ID,但在他们的交易之前,用户 B 将会话变量设置为他们的租户 ID,导致两个数据库请求都写入用户 B 的租户 ID。任何人都可以阐明这是否会成为问题?或者是否有更直接的方式来处理多租户应用程序?

4

1 回答 1

1

我只会在查询函数中过滤 company_id。我知道这正是您不想要的,但我认为多一个参数不会使您的代码混乱。

use crate::schema::{
    products::dsl::{products as all_products},
    products,
};
...

#[derive(Queryable)]
#[table_name="products"]
pub struct Product {
    company_id: i32,
    ...
}

impl Product {
    pub fn all(user: &User, conn: &PgConnection) -> Option<Vec<Product>> {
        all_products.filter(products::company_id.eq(user.get_company_id())).load(conn).ok()
    }

    ...
}

我猜您通过使用用户请求保护来限制对产品页面(或类似页面)的访问。然后你可以将你已经拥有的用户传递给函数。

#[derive(serde::Serialize)]
pub struct AppContext<'a, T> 
where T: 'a {
    body: &'a T,
}

#[get("/product/all", rank = 1)]
pub fn products(conn: DbConn, user: User) -> Template {
    let context: AppContext<'_, Option<Vec<Product>>> = AppContext { 
        body: &Product::all(&user, &conn)  // <= get products
    };
    
    Template::render("products", &context)
}

#[get("/product/all", rank = 2)]
pub fn products_redirect() -> Redirect {
    Redirect::to("/login")
}

此外,如果您的FromRequest实现从数据库中检索用户,您应该考虑使用request.local_cache 。否则,您将在每个请求中至少查询给定用户两次,一次用于视图函数中的用户请求保护,另一次用于TennantView请求保护。

于 2020-08-26T00:01:34.793 回答