mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 19:35:06 +03:00
## Problem The guide alternated between context, procedure, and reference on almost every heading. A reader who wanted to write a policy passed through four context or reference sections to reach one. A reader who wanted the model had to skip three procedures. ## Solution - Group into three sections by information type: `Understand Row Level Security`, `Secure a table with RLS`, and `RLS reference`, with a navigation intro. - Merge the four policy sections. They repeated the same setup block, burying the clause that differed. One setup block now precedes four short policy examples. - Move the auto-enable recipe into `event-triggers.mdx`, whose stub section's entire body was a link back here. - Relocate the stranded `auth.uid()` caution into the `auth.uid()` reference. - Lift the revoke-and-grant procedure out of the danger admonition and merge it with the two other places that taught `enable row level security`. - Point the Grafana IO chart entry at the performance guide. Its `#rls-performance-recommendations` anchor went away when tuning split out in #49016. 765 lines to 582. 30 headings to 25. Headings are demoted rather than renamed wherever anything links to them. Every inbound anchor in the repo still resolves; the only one removed, `#auto-enable-rls-for-new-tables`, was referenced solely by the `event-triggers.mdx` stub this PR replaces. ## Note on the history Rebuilt from `master` after #49011, #49015, and #49016 merged. The branch previously carried those 10 commits plus rebase churn against them. Rebasing naively would have reverted review feedback from #49016 (`70fa812`), which removed the benchmarks table and the "This guide" opener from the performance guide. Those are deliberately not restored here. The only changes to that file are two missing `await`s and a join predicate that was a tautology while unqualified. The three PRs stacked on this one (#49268, #49269, #49270) have been rebased onto the new base. ## Manual testing 1. Open the [Row Level Security guide](https://docs-git-docs-rls-restructure-supabase.vercel.app/docs/guides/database/postgres/row-level-security) on the preview. Three top-level sections appear in the table of contents. 2. Select each link in the intro. All three jump to their section. 3. Open [Event triggers](https://docs-git-docs-rls-restructure-supabase.vercel.app/docs/guides/database/postgres/event-triggers). The auto-enable section holds the full recipe instead of a link. 4. Open the [performance guide](https://docs-git-docs-rls-restructure-supabase.vercel.app/docs/guides/database/postgres/row-level-security-performance). No benchmarks table, and the three bullets at the top link into the RLS guide. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Reworked the Row Level Security guide with clearer guidance on grants, policies, permissions, performance, testing, views, and secure functions. * Added a complete example for automatically enabling RLS on newly created public tables. * Improved SQL examples and clarified table references in RLS performance guidance. * Corrected grammar in the Grafana chart troubleshooting documentation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
561 lines
23 KiB
Plaintext
561 lines
23 KiB
Plaintext
---
|
|
id: 'row-level-security'
|
|
title: 'Row Level Security'
|
|
description: 'Secure your data using Postgres Row Level Security.'
|
|
subtitle: 'Secure your data using Postgres Row Level Security.'
|
|
---
|
|
|
|
Postgres [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html) gives you granular authorization rules that run inside the database.
|
|
|
|
<Admonition type="danger">
|
|
|
|
A table in an exposed schema without RLS is readable and writable by any role with a grant on it. Enable RLS on every table in an exposed schema. On projects that still grant `anon` and `authenticated` by default, revoke those grants. Adding policies doesn't remove them.
|
|
|
|
</Admonition>
|
|
|
|
Use the guide in three parts:
|
|
|
|
- [Understand Row Level Security](#understand-row-level-security) explains how grants and policies combine to control access.
|
|
- [Secure a table with RLS](#secure-a-table-with-rls) is the procedure to follow for every table in an exposed schema.
|
|
- [RLS reference](#rls-reference) documents the helper functions and patterns you use inside a policy expression.
|
|
|
|
Read the first section when you're deciding how to model access. Go directly to the second section when you're ready to secure a table.
|
|
|
|
## Understand Row Level Security
|
|
|
|
### What a policy does
|
|
|
|
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are Postgres's rule engine. Each policy is attached to a table, and the policy is executed every time a table is accessed.
|
|
|
|
Think of a policy as adding a `WHERE` clause to every query. A policy like this:
|
|
|
|
```sql
|
|
create policy "Individuals can view their own todos."
|
|
on todos for select
|
|
to authenticated
|
|
using ( (select auth.uid()) = user_id );
|
|
```
|
|
|
|
That policy translates to this whenever a user selects from the todos table:
|
|
|
|
```sql
|
|
select *
|
|
from todos
|
|
where auth.uid() = todos.user_id;
|
|
-- Policy is implicitly added.
|
|
```
|
|
|
|
You write RLS rules in SQL, so a rule can express whatever access logic your app needs. Because RLS is a Postgres primitive, it also protects your data when it is reached through third-party tooling, which is what makes it "[defense in depth](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)". Combine RLS with [Supabase Auth](/docs/guides/auth) for end-to-end user security from the browser to the database.
|
|
|
|
### Grants and policies
|
|
|
|
Postgres runs two checks before a client touches a table. Grants decide whether a role can run an operation on the table at all. Policies decide which rows that operation applies to. Set both for every table you expose.
|
|
|
|
On existing projects, a new table in `public` starts with every privilege already granted to all three roles:
|
|
|
|
| Role | Granted automatically | What it should keep |
|
|
| --------------- | -------------------------------------- | ------------------------------------------------------- |
|
|
| `anon` | `select`, `insert`, `update`, `delete` | Only what signed-out visitors are meant to read |
|
|
| `authenticated` | `select`, `insert`, `update`, `delete` | Only the operations your app exposes to signed-in users |
|
|
| `service_role` | `select`, `insert`, `update`, `delete` | Full access. It bypasses RLS, so keep it server-side |
|
|
|
|
Adding policies doesn't take those grants back. A table protected only by policies still hands `anon` an insert path if you never revoke the grant.
|
|
|
|
Not every project grants these automatically. See [Default privileges](/docs/guides/api/securing-your-api#default-privileges). Grant each role only the operations it needs.
|
|
|
|
A missing grant raises a `42501` error before any policy runs. When a request fails that your policy should allow, check the grants before you change the policy. To set them, see [Lock down the table](#lock-down-the-table).
|
|
|
|
### Authenticated and unauthenticated roles
|
|
|
|
Supabase maps every request to one of the roles:
|
|
|
|
- `anon`: an unauthenticated request (the user is not logged in)
|
|
- `authenticated`: an authenticated request (the user is logged in)
|
|
|
|
These are [Postgres Roles](/docs/guides/database/postgres/roles). You can use these roles within your Policies using the `TO` clause:
|
|
|
|
```sql
|
|
create policy "Profiles are viewable by everyone"
|
|
on profiles for select
|
|
to authenticated, anon
|
|
using ( true );
|
|
|
|
-- OR
|
|
|
|
create policy "Public profiles are viewable only by authenticated users"
|
|
on profiles for select
|
|
to authenticated
|
|
using ( true );
|
|
```
|
|
|
|
<Admonition type="note" title="Anonymous user vs the anon key">
|
|
|
|
Using the `anon` Postgres role is different from an [anonymous user](/docs/guides/auth/auth-anonymous) in Supabase Auth. An anonymous user assumes the `authenticated` role to access the database and can be differentiated from a permanent user by checking the `is_anonymous` claim in the JWT.
|
|
|
|
</Admonition>
|
|
|
|
A policy that reads `to anon using ( true )` grants every unauthenticated visitor read access to every row the role can already reach through grants. Use it only for data that is meant to be public.
|
|
|
|
### Views and RLS
|
|
|
|
Views bypass RLS by default because they are usually created with the `postgres` user. This is a feature of Postgres, which automatically creates views with `security definer`. A view over a protected table hands out every row its policies were meant to withhold, so a view needs the same attention as a table. To create one safely, see [Expose a view safely](#expose-a-view-safely).
|
|
|
|
## Secure a table with RLS
|
|
|
|
Follow these steps for every table in an exposed schema.
|
|
|
|
### Lock down the table
|
|
|
|
Run these statements in the [SQL Editor](/dashboard/project/_/sql/new) for a one-off change, or in a [migration](/docs/guides/deployment/database-migrations) to keep the change reproducible across environments. Grants and RLS belong in the same migration.
|
|
|
|
Enable RLS, then set the grants to match what each role does in your app:
|
|
|
|
1. Enable RLS on the table.
|
|
|
|
```sql
|
|
alter table public.reports enable row level security;
|
|
```
|
|
|
|
Once RLS is enabled, no data is accessible through the [API](/docs/guides/api) when using a publishable key, until you create policies.
|
|
|
|
2. Revoke any existing grants from both client roles.
|
|
|
|
```sql
|
|
revoke all on table public.reports from anon, authenticated;
|
|
```
|
|
|
|
3. Grant back only the privileges the role needs.
|
|
|
|
```sql
|
|
-- Signed-in users manage reports. Signed-out visitors get nothing.
|
|
grant select, insert, update, delete on table public.reports to authenticated;
|
|
```
|
|
|
|
Data that clients read but never write, such as a feed a backend job populates, gets no write grant at all:
|
|
|
|
```sql
|
|
revoke all on table public.weather_readings from anon, authenticated;
|
|
grant select on table public.weather_readings to anon, authenticated;
|
|
```
|
|
|
|
If new tables still receive automatic grants, see [Revoke default privileges](/docs/guides/api/securing-your-api#revoke-default-privileges). To enable RLS automatically on every new table, see [Event triggers](/docs/guides/database/postgres/event-triggers).
|
|
|
|
Write the tests for this table in the same change. See [Test your policies](#test-your-policies).
|
|
|
|
### Write a policy for each operation
|
|
|
|
Write a separate policy for `select`, `insert`, `update`, and `delete`. Postgres does not accept multiple operations in one `for` clause, and a `for all` policy hides which operation each rule was meant to cover.
|
|
|
|
These examples use a `profiles` table where each user manages only their own row:
|
|
|
|
```sql
|
|
create table profiles (
|
|
id uuid primary key,
|
|
user_id uuid references auth.users,
|
|
avatar_url text
|
|
);
|
|
|
|
alter table profiles enable row level security;
|
|
|
|
revoke all on table profiles from anon, authenticated;
|
|
grant select, insert, update, delete on table profiles to authenticated;
|
|
```
|
|
|
|
Supabase provides [helper functions](#helper-functions) that simplify RLS if you are using Supabase Auth. `auth.uid()` returns the ID of the user making the request.
|
|
|
|
#### SELECT policies
|
|
|
|
You can specify select policies with the `using` clause.
|
|
|
|
```sql
|
|
create policy "Users can view their own profile."
|
|
on profiles for select
|
|
to authenticated
|
|
using ( (select auth.uid()) = user_id );
|
|
```
|
|
|
|
#### INSERT policies
|
|
|
|
You can specify insert policies with the `with check` clause. The `with check` expression ensures that any new row adheres to the policy constraints, so a user cannot create a row that belongs to someone else.
|
|
|
|
```sql
|
|
create policy "Users can create their own profile."
|
|
on profiles for insert
|
|
to authenticated
|
|
with check ( (select auth.uid()) = user_id );
|
|
```
|
|
|
|
#### UPDATE policies
|
|
|
|
You can specify update policies by combining the `using` and `with check` expressions. The `using` clause decides which existing rows can be updated. The `with check` clause decides what the resulting row is allowed to look like, which stops a user from reassigning `user_id` to someone else.
|
|
|
|
```sql
|
|
create policy "Users can update their own profile."
|
|
on profiles for update
|
|
to authenticated
|
|
using ( (select auth.uid()) = user_id ) -- checks the existing row
|
|
with check ( (select auth.uid()) = user_id ); -- checks the resulting row
|
|
```
|
|
|
|
If no `with check` expression is defined, the `using` expression decides both which rows are visible and which new rows are allowed.
|
|
|
|
<Admonition type="caution">
|
|
|
|
To perform an `UPDATE` operation, a corresponding [`SELECT` policy](#select-policies) is required. Without a `SELECT` policy, the `UPDATE` operation will not work as expected.
|
|
|
|
</Admonition>
|
|
|
|
#### DELETE policies
|
|
|
|
You can specify delete policies with the `using` clause.
|
|
|
|
```sql
|
|
create policy "Users can delete their own profile."
|
|
on profiles for delete
|
|
to authenticated
|
|
using ( (select auth.uid()) = user_id );
|
|
```
|
|
|
|
### Specify roles in your policies
|
|
|
|
Always name the role a policy applies to, using the `to` clause. Instead of this:
|
|
|
|
```sql
|
|
create policy "rls_test_select" on rls_test
|
|
using ( auth.uid() = user_id );
|
|
```
|
|
|
|
Use:
|
|
|
|
```sql
|
|
create policy "rls_test_select" on rls_test
|
|
to authenticated
|
|
using ( (select auth.uid()) = user_id );
|
|
```
|
|
|
|
This prevents the policy `( (select auth.uid()) = user_id )` from running for any `anon` users, since the execution stops at the `to authenticated` step.
|
|
|
|
These three rules keep policies correct as a table grows. For the measured impact and for tuning beyond them, see [Row Level Security performance](/docs/guides/database/postgres/row-level-security-performance).
|
|
|
|
### Add indexes
|
|
|
|
Add an [index](/docs/guides/database/postgres/indexes) on every column your policies filter on. Postgres evaluates the policy against each candidate row, so an unindexed filter column turns a read into a sequential scan. For a policy like this:
|
|
|
|
```sql
|
|
create policy "rls_test_select" on test_table
|
|
to authenticated
|
|
using ( (select auth.uid()) = user_id );
|
|
```
|
|
|
|
You can add an index like:
|
|
|
|
```sql
|
|
create index userid
|
|
on test_table
|
|
using btree (user_id);
|
|
```
|
|
|
|
A column counts as indexed only when it comes first in a `btree` index. Postgres can't use a multi-column index to filter on a column that isn't the leading one, so a composite primary key indexes its first column and no others. A membership table keyed on `(team_id, user_id)` has no index on `user_id`:
|
|
|
|
```sql
|
|
create table team_members (
|
|
team_id uuid references teams (id),
|
|
user_id uuid references auth.users (id),
|
|
primary key (team_id, user_id)
|
|
);
|
|
|
|
-- The primary key covers team_id. A policy filtering on user_id needs its own index.
|
|
create index team_members_user_id_idx
|
|
on team_members
|
|
using btree (user_id);
|
|
```
|
|
|
|
### 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.
|
|
|
|
<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>
|
|
|
|
### Expose a view safely
|
|
|
|
In Postgres 15 and above, make a view obey the RLS policies of its underlying tables when invoked by `anon` and `authenticated` by setting `security_invoker = true`.
|
|
|
|
```sql
|
|
create view <VIEW_NAME>
|
|
with(security_invoker = true)
|
|
as select <QUERY>
|
|
```
|
|
|
|
In older versions of Postgres, protect your views by revoking access from the `anon` and `authenticated` roles, or by putting them in an unexposed schema.
|
|
|
|
### Test your policies
|
|
|
|
We recommend writing tests for every policy, in the same change that sets the grants and creates the policies. Tests are a fundamental part of a secure setup, and they give you a repeatable way to prove a policy behaves the way you intended.
|
|
|
|
A wrong policy fails quietly. Too permissive, and a query returns rows it shouldn't. Too strict, and it returns nothing and raises no error. Neither case surfaces as an error, so tests are how you find out.
|
|
|
|
Supabase runs database tests with [pgTAP](/docs/guides/database/extensions/pgtap) through the CLI. Test files are `.sql` files under `supabase/tests/`.
|
|
|
|
#### Anatomy of a policy test
|
|
|
|
Each case sets an identity, runs one statement as that identity, and asserts the outcome. Three things decide whether the assertion means anything.
|
|
|
|
**Identity.** Switch role and identity between cases with `set local role` and `set local request.jwt.claim.sub`, so each assertion runs as the user it describes. Without the switch, every case runs as the same role and proves nothing about access.
|
|
|
|
**Denials.** A denied request doesn't always raise an error, so match the assertion to the way the denial happens:
|
|
|
|
- A missing grant raises `42501`. Assert it with `throws_ok`.
|
|
- A `with check` violation raises `42501`. Assert it with `throws_ok`.
|
|
- A `using` clause that filters the target row out raises nothing. The update or delete matches zero rows instead. Assert that no row changed.
|
|
|
|
**Allowed writes.** The absence of an error doesn't prove that anything changed. Add `returning` to the statement so one assertion covers both directions. An allowed write returns the changed row, and a write the policy filters out returns nothing.
|
|
|
|
#### Write and run the tests
|
|
|
|
1. Create a test file:
|
|
|
|
```bash
|
|
supabase test new profiles_rls
|
|
```
|
|
|
|
2. Write the tests. Cover `select`, `insert`, `update`, and `delete` twice each, once for a request the policy allows and once for a request it denies, for `anon` as well as `authenticated`.
|
|
|
|
[`supabase-test-helpers`](https://github.com/usebasejump/supabase-test-helpers/tree/main) removes most of the setup below. It adds `tests.create_supabase_user()`, `tests.authenticate_as()`, and `tests.rls_enabled()`, so you don't hand-roll user seeding or role switching. See [Advanced pgTAP testing](/docs/guides/local-development/testing/pgtap-extended) for schema-wide assertions and a worked multi-tenant example.
|
|
|
|
3. Run the suite:
|
|
|
|
```bash
|
|
supabase test db
|
|
```
|
|
|
|
This example shows each of those techniques against a `profiles` table where `authenticated` holds every privilege, `anon` holds none, and each user reads and writes only their own row. Extend it to the remaining operations:
|
|
|
|
```sql supabase/tests/profiles_rls.test.sql
|
|
begin;
|
|
select plan(4);
|
|
|
|
insert into auth.users (id, email)
|
|
values
|
|
('11111111-1111-1111-1111-111111111111', 'owner@example.com'),
|
|
('22222222-2222-2222-2222-222222222222', 'other@example.com');
|
|
|
|
-- anon holds no grant, so the request stops before any policy runs.
|
|
set local role anon;
|
|
select throws_ok(
|
|
$$select * from profiles$$,
|
|
'42501',
|
|
null,
|
|
'anon cannot read profiles'
|
|
);
|
|
|
|
-- The owner writes their own row. returning proves the row changed.
|
|
set local role authenticated;
|
|
set local request.jwt.claim.sub = '11111111-1111-1111-1111-111111111111';
|
|
select results_eq(
|
|
$$insert into profiles (id, user_id, avatar_url)
|
|
values (
|
|
gen_random_uuid(),
|
|
'11111111-1111-1111-1111-111111111111',
|
|
'owner.png'
|
|
)
|
|
returning avatar_url$$,
|
|
array['owner.png'],
|
|
'the owner creates their own profile'
|
|
);
|
|
|
|
-- A signed-in stranger holds the grant, so the policy is what stops them. The
|
|
-- using clause filters the row out, so these match nothing and raise nothing.
|
|
set local request.jwt.claim.sub = '22222222-2222-2222-2222-222222222222';
|
|
select is_empty(
|
|
$$select * from profiles$$,
|
|
'another user reads no profiles'
|
|
);
|
|
select is_empty(
|
|
$$update profiles set avatar_url = 'stolen.png' returning avatar_url$$,
|
|
'another user updates no profiles'
|
|
);
|
|
|
|
select * from finish();
|
|
rollback;
|
|
```
|
|
|
|
For CLI setup and more pgTAP helpers, see [Testing your database](/docs/guides/database/testing).
|
|
|
|
## RLS reference
|
|
|
|
These are the functions and patterns available inside a policy expression.
|
|
|
|
### Helper functions
|
|
|
|
Supabase provides some helper functions that make it easier to write policies.
|
|
|
|
#### `auth.uid()`
|
|
|
|
Returns the ID of the user making the request.
|
|
|
|
<Admonition type="caution" title="`auth.uid()` Returns `null` When Unauthenticated">
|
|
|
|
When a request is made without an authenticated user (e.g., no access token is provided or the session has expired), `auth.uid()` returns `null`.
|
|
|
|
This means that a policy like:
|
|
|
|
```sql
|
|
USING (auth.uid() = user_id)
|
|
```
|
|
|
|
will silently fail for unauthenticated users, because:
|
|
|
|
```sql
|
|
null = user_id
|
|
```
|
|
|
|
is always false in SQL.
|
|
|
|
To avoid confusion and make your intention clear, we recommend explicitly checking for authentication:
|
|
|
|
```sql
|
|
USING (auth.uid() IS NOT NULL AND auth.uid() = user_id)
|
|
```
|
|
|
|
</Admonition>
|
|
|
|
#### `auth.jwt()`
|
|
|
|
<Admonition type="caution">
|
|
|
|
Not all information present in the JWT should be used in RLS policies. For instance, creating an RLS policy that relies on the `user_metadata` claim can create security issues in your application as this information can be modified by authenticated end users.
|
|
|
|
</Admonition>
|
|
|
|
Returns the JWT of the user making the request. Anything that you store in the user's `raw_app_meta_data` column or the `raw_user_meta_data` column will be accessible using this function. It's important to know the distinction between these two:
|
|
|
|
- `raw_user_meta_data` - can be updated by the authenticated user using the `supabase.auth.update()` function. It is not a good place to store authorization data.
|
|
- `raw_app_meta_data` - cannot be updated by the user, so it's a good place to store authorization data.
|
|
|
|
The `auth.jwt()` function is extremely versatile. For example, if you store some team data inside `app_metadata`, you can use it to determine whether a particular user belongs to a team. For example, if this was an array of IDs:
|
|
|
|
```sql
|
|
create policy "User is in team"
|
|
on my_table
|
|
to authenticated
|
|
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));
|
|
```
|
|
|
|
<Admonition type="caution">
|
|
|
|
Keep in mind that a JWT is not always up-to-date. In the team policy example, even if you remove a user from a team and update the `app_metadata` field, that will not be reflected using `auth.jwt()` until the user's JWT is refreshed.
|
|
|
|
Also, if you are using Cookies for Auth, then you must be mindful of the JWT size. Some browsers are limited to 4096 bytes for each cookie, and so the total size of your JWT should be small enough to fit inside this limitation.
|
|
|
|
</Admonition>
|
|
|
|
#### MFA
|
|
|
|
The `auth.jwt()` function can be used to check for [Multi-Factor Authentication](/docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins). For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):
|
|
|
|
```sql
|
|
create policy "Restrict updates."
|
|
on profiles
|
|
as restrictive
|
|
for update
|
|
to authenticated using (
|
|
(select auth.jwt()->>'aal') = 'aal2'
|
|
);
|
|
```
|
|
|
|
### 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` privileges. 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 (select 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
|
|
set search_path = '' -- every name inside must be schema-qualified
|
|
as $$
|
|
begin
|
|
return exists (
|
|
select 1 from public.roles_table
|
|
where (select 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 ( (select private.has_good_role()) );
|
|
```
|
|
|
|
Set `search_path = ''` on every `security definer` function and schema-qualify the names inside it. Without a pinned `search_path`, a caller can point an unqualified name at their own object and run it with the function owner's privileges.
|
|
|
|
<Admonition type="caution">
|
|
|
|
A `security definer` function in an exposed schema is callable over the Data API with the creator's privileges. Never create one in a schema listed under "Exposed schemas" in your [API settings](/dashboard/project/_/settings/api).
|
|
|
|
</Admonition>
|
|
|
|
### Bypassing Row Level Security
|
|
|
|
Use a [secret key](/docs/guides/getting-started/api-keys) for administrative tasks that need to bypass RLS. A secret key authorizes access through the `service_role` Postgres role, which has the `bypassrls` attribute. Never use a secret key in the browser or expose it to customers.
|
|
|
|
The JWT-based `service_role` key is a legacy alternative. Prefer a secret key where possible.
|
|
|
|
<Admonition type="note">
|
|
|
|
A secret key bypasses RLS only when the request carries no user access token. If the request carries one, it runs under the RLS policies of that signed-in user, even when the client library was initialized with a secret key.
|
|
|
|
</Admonition>
|
|
|
|
You can also create new [Postgres Roles](/docs/guides/database/postgres/roles) which can bypass Row Level Security using the "bypass RLS" privilege:
|
|
|
|
```sql
|
|
alter role "role_name" with bypassrls;
|
|
```
|
|
|
|
This can be useful for system-level access. **Never** share login credentials for any Postgres Role with this privilege.
|
|
|
|
## Related content
|
|
|
|
- [Row Level Security performance](/docs/guides/database/postgres/row-level-security-performance): diagnose whether policies are your bottleneck, and tune ones that are already correct.
|
|
- [Advanced pgTAP testing](/docs/guides/local-development/testing/pgtap-extended): schema-wide RLS test helpers and a worked multi-tenant example.
|
|
- [Testing your database](/docs/guides/database/testing): the CLI test workflow that `supabase test db` runs.
|
|
- [Securing your API](/docs/guides/api/securing-your-api): grants, dedicated schemas, and pre-request checks around the Data API.
|
|
- [Column Level Security](/docs/guides/database/postgres/column-level-security): restrict access to individual columns.
|
|
- [`supabase-test-helpers`](https://github.com/usebasejump/supabase-test-helpers/tree/main): a community extension that adds user creation and role impersonation helpers to pgTAP.
|