diff --git a/apps/docs/pages/guides/database/postgres/row-level-security.mdx b/apps/docs/pages/guides/database/postgres/row-level-security.mdx index 56cb2c5478c..9f887a04b46 100644 --- a/apps/docs/pages/guides/database/postgres/row-level-security.mdx +++ b/apps/docs/pages/guides/database/postgres/row-level-security.mdx @@ -108,6 +108,54 @@ to authenticated -- the Postgres Role (recommended) using ( auth.id() = user_id ); -- the actual Policy ``` +### UPDATE Policies + +You can specify select policies with the `using` command. + +Let's say you have a table called `profiles` in the public schema and you only want users to be able to update their own profile: + +```sql +-- 1. Create table +create table profiles ( + id uuid primary key, + user_id references auth.users, + avatar_url text +); + +-- 2. Enable RLS +alter table profiles enable row level security; + +-- 3. Create Policy +create policy "Users can update their own profile." +on profiles for update +to authenticated -- the Postgres Role (recommended) +using ( auth.id() = user_id ); -- the actual Policy +``` + +### DELETE Policies + +You can specify select policies with the `using` command. + +Let's say you have a table called `profiles` in the public schema and you only want users to be able to delete their own profile: + +```sql +-- 1. Create table +create table profiles ( + id uuid primary key, + user_id references auth.users, + avatar_url text +); + +-- 2. Enable RLS +alter table profiles enable row level security; + +-- 3. Create Policy +create policy "Users can delete a profile." +on profiles for delete +to authenticated -- the Postgres Role (recommended) +using ( auth.id() = user_id ); -- the actual Policy +``` + ## Bypassing Row Level Security You can create [Postgres Roles](/docs/guides/database/postgres/roles) which can bypass Row Level Security using the "bypass RLS" privelege: @@ -118,9 +166,133 @@ grant bypassrls on "table_name" to "role_name"; This can be useful for system-level access. You should _never_ share login credentials for any Postgres Role with this privelege. -## RLS Performance +## RLS Performance Recommendations -TBD +Every authorization system has an impact on Performance. While Row Level Security is powerful, the performance impact is important to keep in mind. This is especially true for queries that scan every row in a table - like many `select` operations, including those using limit, offset, and ordering. + +Based on a series of [tests](https://github.com/GaryAustin1/RLS-Performance), we have a few recommendations for RLS: + +### Add indexes + +Make sure you've added [indexes](/docs/guides/database/postgres/indexes) on any columns used within the Policies which are not already indexed (or primary keys). For a Policy like this: + +```sql +create policy "rls_test_select" +on test_table +to authenticated +using ( auth.uid() = user_id ); +``` + +You can add an index like: + +```sql +create index userid +on test_table +using btree (user_id); +``` + +#### Benchmarks + +| Test | Before (ms) | After (ms) | % Improvement | Change | +| --------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------- | +| [test1-indexed](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test1-indexed) | 171 | < 0.1 | 99.94% |
Before:
No index

After:
`user_id` indexed
| + +### Call functions with `select` + +You can use `select` statement to improve Policies that use Functions. For example, instead of this: + +```sql +create policy "rls_test_select" +on test_table +to authenticated +using ( auth.uid() = user_id ); +``` + +You can do: + +```sql +create policy "rls_test_select" +on test_table +to authenticated +using ( (select auth.uid()) = user_id ); +``` + +This method works well for JWT functions like `auth.uid()` and `auth.jwt()` as well as `security definer` Functions. Wrapping the function causes an initPlan to be run by the Postgres optimizer, which allows it to "cache" the results per-statement, rather than calling the function on each row. + +{/* prettier-ignore */} + +You can only use this technique if the results of the query or function do not change based on the row data. + + +#### Benchmarks + +| Test | Before (ms) | After (ms) | % Improvement | Change | +| --------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [test2a-wrappedSQL-uid]() | 179 | 9 | 94.97% |
Before:
`auth.uid() = user_id`

After:
`(select auth.uid()) = user_id`
| +| [test2b-wrappedSQL-isadmin]() | 11,000 | 7 | 99.94% |
Before:
`is_admin()` _table join_

After:
`(select is_admin())` _table join_
| +| [test2c-wrappedSQL-two-functions](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2c-wrappedSQL-two-functions) | 11,000 | 10 | 99.91% |
Before:
`is_admin() OR auth.uid() = user_id`

After:
`(select is_admin()) OR (select auth.uid() = user_id)`
| +| [test2d-wrappedSQL-sd-fun](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2d-wrappedSQL-sd-fun) | 178,000 | 12 | 99.993% |
Before:
`has_role() = role`

After:
(select has_role()) = role
| +| [test2e-wrappedSQL-sd-fun-array](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2e-wrappedSQL-sd-fun-array) | 173000 | 16 | 99.991% |
Before:
`team_id=any(user_teams())`

After:
team_id=any(array(select user_teams()))
| + +### Add filters to every query + +Policies are "implicit where clauses", so it's common to run `select` statements without any filters. This is a bad pattern for performance. Instead of doing this (JS client example): + +```js +const { data } = supabase.from('table').select() +``` + +You should always add a filter: + +```js +const { data } = supabase.from('table').select().eq('user_id', userId) +``` + +Even if this duplicates the contents of the Policy, Postgres can use the to construct a better query plan. + +## Use security definer functions + +A "security definer" function runs using the same role that _created_ the function. This means that if you create a role with a superuser (like `postgres`), then that function will have `bypassrls` priveleges. For example, if you had a policy like this: + +```sql +create policy "rls_test_select" +on test_table +to authenticated +using ( + exists ( + select 1 from roles_table + where auth.uid() = user_id and role = 'good_role' + ) +); +``` + +We can instead create a `security definer` function which can scan `roles_table` without any RLS penalties: + +```sql +create function private.has_good_role() +returns boolean +language plpgsql +security definer -- will run as the creator +as $$ +begin + return exists ( + select 1 from roles_table + where auth.uid() = user_id and role = 'good_role' + ) +end; +$$; + +-- Update our policy to use this function: +create policy "rls_test_select" +on test_table +to authenticated +using ( private.has_good_role() ); +``` + +{/* prettier-ignore */} + +Security-definer functions should never be created in a schema in the "Exposed schemas" inside your [API settings](https://supabase.com/dashboard/project/_/settings/api)`. + export const Page = ({ children }) =>