Adds some performance items

This commit is contained in:
Copple committed 2023-08-22 23:18:25 +02:00
1 parent f8e4420929
commit 962d0e1edc
1 file changed
+174 -2
@@ -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% | <details className="cursor-pointer">Before:<br/>No index<br/><br/>After:<br/>`user_id` indexed</details> |
### 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 */}
<Admonition type="caution">
You can only use this technique if the results of the query or function do not change based on the row data.
</Admonition>
#### Benchmarks
| Test | Before (ms) | After (ms) | % Improvement | Change |
| --------------------------------------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [test2a-wrappedSQL-uid](<https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2a-wrappedSQL-uid()>) | 179 | 9 | 94.97% | <details className="cursor-pointer">Before:<br/>`auth.uid() = user_id` <br/><br/>After:<br/> `(select auth.uid()) = user_id`</details> |
| [test2b-wrappedSQL-isadmin](<https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2b-wrappedSQL-isadmin()>) | 11,000 | 7 | 99.94% | <details className="cursor-pointer">Before:<br/>`is_admin()` _table join_<br/><br/>After:<br/>`(select is_admin())` _table join_</details> |
| [test2c-wrappedSQL-two-functions](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2c-wrappedSQL-two-functions) | 11,000 | 10 | 99.91% | <details className="cursor-pointer">Before:<br/>`is_admin() OR auth.uid() = user_id`<br/><br/>After:<br/>`(select is_admin()) OR (select auth.uid() = user_id)`</details> |
| [test2d-wrappedSQL-sd-fun](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2d-wrappedSQL-sd-fun) | 178,000 | 12 | 99.993% | <details className="cursor-pointer">Before:<br/>`has_role() = role` <br/><br/>After:<br/>(select has_role()) = role</details> |
| [test2e-wrappedSQL-sd-fun-array](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test2e-wrappedSQL-sd-fun-array) | 173000 | 16 | 99.991% | <details className="cursor-pointer">Before:<br/>`team_id=any(user_teams())` <br/><br/>After:<br/>team_id=any(array(select user_teams()))</details> |
### 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 */}
<Admonition type="caution">
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)`.
</Admonition>
export const Page = ({ children }) => <Layout meta={meta} children={children} />