RLS support #1587
Replies: 9 comments 6 replies
-
|
This definitely something we're interested in. Getting to a stable, scalable 1.0 is our immediate priority (we'll rely on APIs for auth until then) but we're keen to explore other options in the future including RLS. |
Beta Was this translation helpful? Give feedback.
-
|
Just for some addition context, this is something we've looked into in some depth. Your summary is exactly right:
There are two primary approaches for solving (2): a. find a way to use a replication stream per tenant / auth context; this can work for some data access patterns The second approach requires getting the rules and running them against the operations in the replication stream. This has a challenge, in that RLS rules are quite flexible/expressive and allow you to query the state of the database. For example, when authorising access to a row, you can query another table. This is challenging when processing the replication stream, because the replication stream is behind the current state of the database. Postgres has MVCC internally but does not (natively) allow you to query the database at a particular snapshot. This means that by the time you have received a transaction in the replication stream, you can't query the database within the same snapshot. This limits the complexity of RLS rules that can be supported. Possible solutions
|
Beta Was this translation helpful? Give feedback.
-
|
Especially as you're mentioning supabase, it would be cool if electric could just leverage RLS policies that are created in supabase already. This way an offline enabled app with electric + supabase could have quite a head start. |
Beta Was this translation helpful? Give feedback.
-
|
One of the observations about the https://supabase.com/docs/guides/realtime/authorization design is that we could potentially support something similar, where RLS rules set on an |
Beta Was this translation helpful? Give feedback.
-
|
Any progress on this matter? This limitation is preventing us from adopting electic's sync engine. |
Beta Was this translation helpful? Give feedback.
-
|
We’ve actually gone a bit more in the opposite direction: https://electric-sql.com/docs/guides/auth#it-s-all-http Rather than codifying auth logic in database rules, Electric allows you to filter and authorize data access in your web framework / middleware, much as you would any other web resource like a REST or GraphQL endpoint. You can use RLS to filter the data that the Electric dbuser sees but you don’t need to express auth logic in database rules. Obviously if you have an investment in existing RLS rules, this may be less useful / attractive. And in the longer run we may implement some user aware connection support, a bit like Nile’s tenant context or similar. However, right now we’re leaning into the more web-native approach of “it’s all just HTTP and the shape definition is in the request params”. |
Beta Was this translation helpful? Give feedback.
-
Same for me: lack of RLS + lack of joins + lack of views + only trivial filtering are all deal breakers. IMO currently Electric looks like a mere "single table downloader", which is still something, but:
IMO PGLite + this at 1.0, ideally as a drop-in thing rather than super verbose tables declaration thing, would be the real BOMB and can really shake the foundations towards local-first/offline-first web apps finally being efforless. |
Beta Was this translation helpful? Give feedback.
-
|
Hi! |
Beta Was this translation helpful? Give feedback.
-
|
I understand the syntax for "where" is limited, but it's not a dealbreaker IMO. I am currently doing the filtering like this. Is there anything wrong with that approach? app.get("/v1/shape", async (c) => {
const {
req,
var: { userId },
} = c;
const originUrl = new URL(`${env(c).ELECTRIC_ORIGIN_URL}/v1/shape`);
const requestUrl = new URL(req.url);
const columns = requestUrl.searchParams.get("columns")?.split(",") ?? [];
if (!columns.length) {
return c.json({ error: "No columns specified" }, 400);
}
const columnsToKeep = [...new Set(["id", ...columns])];
requestUrl.searchParams.forEach((value, key) => {
originUrl.searchParams.set(key, value);
});
requestUrl.searchParams.delete("columns");
requestUrl.searchParams.set("columns", columnsToKeep.join(","));
const table = originUrl.searchParams.get("table") as keyof DB | null;
if (!table) {
return c.json({ error: "No table specified" }, 400);
}
const organizationIds = await db
.selectFrom("organizations")
.select("id")
.where(buildOrganizationsWhere({ userId, action: "select" }))
.execute();
const userOrganizationIds = organizationIds.map((o) => o.id);
console.log("User organization ids", userOrganizationIds.length);
// Preliminary filtering should provide a huge performance boost
// it also avoids the penalty of the customer base growing affecting the sync for every single user
// because without this, all sync sessions for all users will have to iterate through every single table row
// to check if they are allowed to see it.
let preliminaryWhere = "";
let preliminaryWhereParams: string[] = [];
switch (table) {
case "organizations": {
if (!userOrganizationIds.length) {
preliminaryWhere = "false";
break;
}
preliminaryWhere = `id IN (${userOrganizationIds.map((id, index) => `$${index + 1}`).join(",")})`;
preliminaryWhereParams = userOrganizationIds;
break;
}
case "todos":
case "todo_lists": {
if (!userOrganizationIds.length) {
preliminaryWhere = "false";
break;
}
preliminaryWhere = `organization_id IN (${userOrganizationIds.map((id, index) => `$${index + 1}`).join(",")})`;
preliminaryWhereParams = userOrganizationIds;
break;
}
default:
break;
}
if (preliminaryWhere) {
originUrl.searchParams.set("where", preliminaryWhere);
for (let i = 0; i < preliminaryWhereParams.length; i++) {
originUrl.searchParams.set(`params[${i + 1}]`, preliminaryWhereParams[i]);
}
}
const response = await fetch(originUrl.toString());
if (!response.ok) {
return response;
}
const responseBody = (await response.json()) as any[];
const rowsToCheck = responseBody.filter((item) => {
// Control messages and other non-row data don't have a `value` property.
// We should always pass these through.
if (!item.value || !item.value.id) {
return false;
}
return true;
}) as { value: { id: string } }[];
const checkResults = await selectAccessibleRowsFromTable(
table,
new Set(rowsToCheck.map((item) => item.value.id)),
userId,
db
);
console.log(
`Check results for table ${table} (num accessible rows): ${checkResults.size} / ${rowsToCheck.length}`
);
// Filter the original response array based on the permission check results.
const filteredBody = responseBody.filter((item, index) => {
if (!item.value || !item.value.id) {
return true;
}
return checkResults.has(item.value.id);
});
// Return the filtered array as a new JSON response.
// We also forward the original headers from Electric.
const headers = new Headers(response.headers);
// Recalculate content-length as we have modified the body.
headers.delete("content-length");
return c.json(filteredBody, { headers });
});Of course, having more complex |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I wonder if you have any plans to support Row Level Security (RLS)?
Most uses of RLS at scale use a single user (or a few users corresponding to roles), and use SET LOCAL variables to describe the current user ID. See https://www.graphile.org/postgraphile/security/#how-it-works
So it would be a matter of:
Beta Was this translation helpful? Give feedback.
All reactions