I have a multi-tenant web app that may need to support dozens of tenants (companies). I've been looking for a way to ensure that a tenant only has access to their own data (its important that there's no leaks) without having to pass around the tenant_id
to every form and SQL query. My idea was to create an updatable view so that a user's query can only operate within the bounds of their company's data.
I'm doing this by creating a view (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);
This creates an updatable view that only allows queries on that company's data, without having to specify their tenant_id
.
In Diesel, I've written the table! macros for the views so Diesel sees them as tables. In Rocket, I've wrapped my database connection Request Guard in another Request Guard that first sends a SQL query to set my.tenant_id
to the user's 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))
}
}
The user can then use the request guard to make database queries and will only have access to their company's data. A tenant specific updatable view.
BUT my concern is this could result in a race condition. I'm not clear on how postgres session variables work with Diesel and Rocket. Suppose users from two different companies simultaneously submit a request to Rocket, and the session variable for User A is set to their tenant id, but before their transaction, User B sets the session variable to THEIR tenant id, causing both database requests to write to User B's tenant id. Can anyone shed some light on if this would be a problem or not? Or if there's a more straight forward way to handle multi tenant apps?