Merge pull request #16704 from supabase/doc/rls_recommendations

Docs: RLS and Roles
This commit is contained in:
Copple authored and GitHub committed 2023-08-24 14:31:17 +02:00
commit 37ff57cfe0
17 files changed
+1006 -588

No files matched your search

@@ -545,34 +545,35 @@ export const database: NavMenuConstant = {
items: [
{ name: 'Connecting to your database', url: '/guides/database/connecting-to-postgres' },
{ name: 'Managing tables, views, and data', url: '/guides/database/tables' },
{ name: 'JSON and unstructured data', url: '/guides/database/json' },
{ name: 'Managing database functions', url: '/guides/database/functions' },
{ name: 'Managing indexes', url: '/guides/database/postgres/indexes' },
{ name: 'Managing database triggers', url: '/guides/database/postgres/triggers' },
{ name: 'Managing database webhooks', url: '/guides/database/webhooks' },
{ name: 'Managing database replication', url: '/guides/database/replication' },
{ name: 'Using Full Text Search', url: '/guides/database/full-text-search' },
],
},
{
name: 'Access and security',
url: undefined,
items: [
{ name: 'Row Level Security', url: '/guides/database/postgres/row-level-security' },
{ name: 'Managing Postgres Roles', url: '/guides/database/postgres/roles' },
{ name: 'Managing secrets with Vault', url: '/guides/database/vault' },
{ name: 'Column encryption', url: '/guides/database/column-encryption' },
],
},
{
name: 'Postgres Guides',
url: undefined,
items: [
{
name: 'Analyzing efficiency and performance',
url: '/guides/database/inspect',
},
{
name: 'JSON and unstructured data',
url: '/guides/database/json',
},
{ name: 'Implementing Full Text Search', url: '/guides/database/full-text-search' },
{ name: 'Implementing Cascade Deletes', url: '/guides/database/postgres/cascade-deletes' },
{ name: 'Implementing column encryption', url: '/guides/database/column-encryption' },
{ name: 'Partitioning your tables', url: '/guides/database/partitions' },
{ name: 'Query Optimization', url: '/guides/database/query-optimization' },
{ name: 'Debugging and monitoring', url: '/guides/database/inspect' },
{ name: 'Partitioning your tables', url: '/guides/database/partitions' },
{ name: 'Implementing Cascade Deletes', url: '/guides/database/postgres/cascade-deletes' },
{ name: 'Managing database replication', url: '/guides/database/replication' },
{ name: 'Testing your database', url: '/guides/database/testing' },
{ name: 'Managing Timeouts', url: '/guides/database/timeouts' },
{ name: 'Managing Passwords', url: '/guides/database/managing-passwords' },
{ name: 'Configuring Timezones', url: '/guides/database/managing-timezones' },
{ name: 'Database Configuration', url: '/guides/database/postgres/configuration' },
],
},
{
+1
View File
@@ -159,6 +159,7 @@ const Layout: FC<Props> = (props) => {
<div className="border-l">
{props.meta?.tocVideo && !!tocVideoPreview && (
<div className="relative mb-6 pl-5">
<h1>{props.meta.tocVideo}</h1>
<ExpandableVideo imgUrl={tocVideoPreview} videoId={props.meta.tocVideo} />
</div>
)}
+37 -109
View File
@@ -8,17 +8,11 @@ export const meta = {
}
Multi-factor authentication (MFA), sometimes called two-factor
authentication (2FA), adds an additional layer of security to your
application by verifying their identity through
additional verification steps.
Multi-factor authentication (MFA), sometimes called two-factor authentication (2FA), adds an additional layer of security to your application by verifying their identity through additional verification steps.
It is considered a best practice to use MFA for your applications.
Users with weak passwords or compromised social login accounts are
prone to malicious account takeovers. These can be prevented with
MFA because they require the user to provide proof of
both of these:
Users with weak passwords or compromised social login accounts are prone to malicious account takeovers. These can be prevented with MFA because they require the user to provide proof of both of these:
- Something they know.
Password, or access to a social-login account.
@@ -27,9 +21,7 @@ both of these:
## Overview
Supabase Auth implements only Time-based One Time Factor(TOTP) multi-factor authentication. This type of
multi-factor authentication uses a timed one-time password generated from an
authenticator app in the control of users.
Supabase Auth implements only Time-based One Time Password(TOTP) multi-factor authentication. This type of multi-factor authentication uses a timed one-time password generated from an authenticator app in the control of users.
Applications using MFA require two important flows:
@@ -44,8 +36,7 @@ Supabase Auth provides:
- **Challenge and Verify APIs** - securely verify that the user has access to a factor.
- **List Factors API** - build rich user interfaces for signing in with additional factors.
Below is a flow chart illustrating how these APIs work together to enable MFA
features in your app.
Below is a flow chart illustrating how these APIs work together to enable MFA features in your app.
<Mermaid
chart={`
@@ -59,18 +50,11 @@ graph TD;
`}
/>
These sets of APIs let you control the MFA experience that works for you. You
can create flows where MFA is optional, mandatory for all or only specific
groups of users.
These sets of APIs let you control the MFA experience that works for you. You can create flows where MFA is optional, mandatory for all, or only specific groups of users.
Once users have enrolled or signed-in with a factor, Supabase Auth adds
additional metadata to the user's access token (JWT) that your application can
use to allow or deny access.
Once users have enrolled or signed-in with a factor, Supabase Auth adds additional metadata to the user's access token (JWT) that your application can use to allow or deny access.
This information is represented by an [Authenticator Assurance
Level](https://pages.nist.gov/800-63-3-Implementation-Resources/63B/AAL/), a
standard measure about the assurance Supabase Auth has of the user's identity
for that particular session. There are two levels recognized today:
This information is represented by an [Authenticator Assurance Level](https://pages.nist.gov/800-63-3-Implementation-Resources/63B/AAL/), a standard measure about the assurance of the user's identity Supabase Auth has for that particular session. There are two levels recognized today:
1. **Assurance Level 1: `aal1`**
Means that the user's identity was verified using a conventional login method
@@ -80,10 +64,7 @@ for that particular session. There are two levels recognized today:
Means that the user's identity was additionally verified using at least one
second factor, such as a TOTP code.
This assurance level is encoded in the `aal` claim in the JWT associated with
the user. By decoding this value you can create custom authorization rules in
your frontend, backend and database that will enforce the MFA policy that works
for your application. JWTs without an `aal` claim are at the `aal1` level.
This assurance level is encoded in the `aal` claim in the JWT associated with the user. By decoding this value you can create custom authorization rules in your frontend, backend, and database that will enforce the MFA policy that works for your application. JWTs without an `aal` claim are at the `aal1` level.
## Adding to your app
@@ -107,8 +88,7 @@ Adding MFA to your app involves these four steps:
### Add enrollment flow
An enrollment flow provides a UI for users to set up additional authentication factors.
Most applications add the enrollment flow in two places within their app:
An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places within their app:
1. Right after login or sign up.
This lets users quickly set up MFA immediately after they log in or create an
@@ -118,8 +98,7 @@ Most applications add the enrollment flow in two places within their app:
2. From within a settings page.
Allows users to set up, disable or modify their MFA settings.
We recommend building one generic flow that you can reuse in both cases with
minor modifications.
We recommend building one generic flow that you can reuse in both cases with minor modifications.
Enrolling a factor for use with MFA takes three steps:
@@ -140,8 +119,7 @@ Enrolling a factor for use with MFA takes three steps:
#### Example: React
Below is an example that creates a new `EnrollMFA` component that illustrates
the important pieces of the MFA enrollment flow.
Below is an example that creates a new `EnrollMFA` component that illustrates the important pieces of the MFA enrollment flow.
- When the component appears on screen, the `supabase.auth.mfa.enroll()` API is
called once to start the process of enrolling a new factor for the current
@@ -240,18 +218,14 @@ export function EnrollMFA({
### Add unenrollment flow
An unenrollment flow provides a UI for users to manage and unenroll factors linked to their accounts.
Most applications do so via a factor management page where users can view and unlink selected factors.
An unenrollment flow provides a UI for users to manage and unenroll factors linked to their accounts. Most applications do so via a factor management page where users can view and unlink selected factors.
When a user unenrolls a factor, call `supabase.auth.mfa.unenroll()` with the ID of the factor.
For example, call `supabase.auth.mfa.unenroll({factorId: "d30fd651-184e-4748-a928-0a4b9be1d429"})` to unenroll a factor with ID `d30fd651-184e-4748-a928-0a4b9be1d429`.
When a user unenrolls a factor, call `supabase.auth.mfa.unenroll()` with the ID of the factor. For example, call `supabase.auth.mfa.unenroll({factorId: "d30fd651-184e-4748-a928-0a4b9be1d429"})` to unenroll a factor with ID `d30fd651-184e-4748-a928-0a4b9be1d429`.
#### Example: React
Below is an example that creates a new `UnenrollMFA` component that illustrates
the important pieces of the MFA enrollment flow. Note that users can only unenroll a factor after completing the enrollment flow and obtaining
an `aal2` JWT claim.Here are some points of note:
Below is an example that creates a new `UnenrollMFA` component that illustrates the important pieces of the MFA enrollment flow. Note that users can only unenroll a factor after completing the enrollment flow and obtaining an `aal2` JWT claim. Here are some points of note:
- When the component appears on screen, the `supabase.auth.mfa.listFactors()` endpoint
fetches all existing factors together with their details.
@@ -317,17 +291,11 @@ export function UnenrollMFA() {
### Add challenge step to login
Once a user has logged in via their first factor (email+password, magic link,
one time password, social login...) you need to perform a check if any
additional factors need to be verified.
Once a user has logged in via their first factor (email+password, magic link, one time password, social login etc.) you need to perform a check if any additional factors need to be verified.
This can be done by using the
`supabase.auth.mfa.getAuthenticatorAssuranceLevel()` API. When the user signs
in and is redirected back to your app, you should call this method to extract
the user's current and next authenticator assurance level (AAL).
This can be done by using the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` API. When the user signs in and is redirected back to your app, you should call this method to extract the user's current and next authenticator assurance level (AAL).
Therefore if you receive a `currentLevel` which is `aal1` but a `nextLevel`
of `aal2`, the user should be given the option to go through MFA.
Therefore if you receive a `currentLevel` which is `aal1` but a `nextLevel` of `aal2`, the user should be given the option to go through MFA.
Below is a table that explains the combined meaning.
@@ -340,14 +308,9 @@ Below is a table that explains the combined meaning.
#### Example: React
Adding the challenge step to login depends heavily on the architecture of your
app. However, a fairly common way to structure React apps is to have a large
component (often named `App`) which contains most of the authenticated
application logic.
Adding the challenge step to login depends heavily on the architecture of your app. However, a fairly common way to structure React apps is to have a large component (often named `App`) which contains most of the authenticated application logic.
This example will wrap this component with logic that will show an MFA
challenge screen if necessary, before showing the full application. This is
illustrated in the `AppWithMFA` example below.
This example will wrap this component with logic that will show an MFA challenge screen if necessary, before showing the full application. This is illustrated in the `AppWithMFA` example below.
```tsx
function AppWithMFA() {
@@ -468,12 +431,9 @@ function AuthMFA() {
### Enforce rules for MFA logins
Adding MFA to your app's UI does not in-and-of-itself offer a higher level of
security to your users. You also need to enforce the MFA rules in your
application's database, APIs and server-side rendering.
Adding MFA to your app's UI does not in-and-of-itself offer a higher level of security to your users. You also need to enforce the MFA rules in your application's database, APIs, and server-side rendering.
Depending on your application's needs, there are three ways you can choose to
enforce MFA.
Depending on your application's needs, there are three ways you can choose to enforce MFA.
1. **Enforce for all users (new and existing).**
Any user account will have to enroll MFA to continue using your app.
@@ -489,21 +449,17 @@ enforce MFA.
#### Database
Your app should sufficiently deny or allow access to tables or rows based on
the user's current and possible authenticator levels.
Your app should sufficiently deny or allow access to tables or rows based on the user's current and possible authenticator levels.
<Admonition type="caution">
PostgreSQL has two types of policies: permissive and restrictive. This guide
uses restrictive policies. Make sure you don't omit the `as restrictive`
clause.
PostgreSQL has two types of policies: permissive and restrictive. This guide uses restrictive policies. Make sure you don't omit the `as restrictive` clause.
</Admonition>
##### Enforce for all users (new and existing)
If your app falls under this case, this is a template Row Level Security policy
you can apply to all your tables:
If your app falls under this case, this is a template Row Level Security policy you can apply to all your tables:
```sql
create policy "Policy name."
@@ -520,9 +476,7 @@ create policy "Policy name."
##### Enforce for new users only
If your app falls under this case, the rules get more complex. User accounts
created past a certain timestamp must have a `aal2` level to access the
database.
If your app falls under this case, the rules get more complex. User accounts created past a certain timestamp must have a `aal2` level to access the database.
```sql
create policy "Policy name."
@@ -582,23 +536,15 @@ create policy "Policy name."
<Admonition type="tip">
When using the Supabase JavaScript library in a server-side rendering context,
make sure you always create a new object for each request! This will prevent
you from accidentally rendering and serving content belonging to different
users.
When using the Supabase JavaScript library in a server-side rendering context, make sure you always create a new object for each request! This will prevent you from accidentally rendering and serving content belonging to different users.
</Admonition>
It is possible to enforce MFA on the Server-Side Rendering level. However, this
can be tricky do to well.
It is possible to enforce MFA on the Server-Side Rendering level. However, this can be tricky do to well.
You can use the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` and
`supabase.auth.mfa.listFactors()` APIs to identify the AAL level of the session
and any factors that are enabled for a user, similar to how you would use these
on the browser.
You can use the `supabase.auth.mfa.getAuthenticatorAssuranceLevel()` and `supabase.auth.mfa.listFactors()` APIs to identify the AAL level of the session and any factors that are enabled for a user, similar to how you would use these on the browser.
However, encountering a different AAL level on the server may not actually be a
security problem. Consider these likely scenarios:
However, encountering a different AAL level on the server may not actually be a security problem. Consider these likely scenarios:
1. User signed-in with a conventional method but closed their tab on the MFA
flow.
@@ -607,16 +553,11 @@ security problem. Consider these likely scenarios:
3. User has lost their authenticator device and is confused about the next
steps.
We thus recommend you redirect users to a page where they can authenticate
using their additional factor, instead of rendering a HTTP 401 Unauthorized or
HTTP 403 Forbidden content.
We thus recommend you redirect users to a page where they can authenticate using their additional factor, instead of rendering a HTTP 401 Unauthorized or HTTP 403 Forbidden content.
### APIs
If your application uses the Supabase Database, Storage or Edge Functions, just
using Row Level Security policies will give you sufficient protection. In the
event that you have other APIs that you wish to protect, follow these general
guidelines:
If your application uses the Supabase Database, Storage or Edge Functions, just using Row Level Security policies will give you sufficient protection. In the event that you have other APIs that you wish to protect, follow these general guidelines:
1. **Use a good JWT verification and parsing library for your language.**
This will let you securely parse JWTs and extract their claims.
@@ -632,28 +573,17 @@ guidelines:
### Why is there a challenge and verify API when challenge does not do much?
TOTP is not going to be the only MFA factor Supabase Auth is going to support
in the future. By separating out the challenge and verify steps, we're making
the library forward compatible with new factors we may add in the future --
such as SMS or WebAuthn. For example, for SMS the `challenge` endpoint would
actually send out the SMS with the authentication code.
TOTP is not going to be the only MFA factor Supabase Auth is going to support in the future. By separating out the challenge and verify steps, we're making the library forward compatible with new factors we may add in the future -- such as SMS or WebAuthn. For example, for SMS the `challenge` endpoint would actually send out the SMS with the authentication code.
### What's inside the QR code?
The TOTP QR code encodes a URI with the `otpauth` scheme. It was [initially
introduced by Google
Authenticator](https://github.com/google/google-authenticator/wiki/Key-Uri-Format)
but is now universally accepted by all authenticator apps.
The TOTP QR code encodes a URI with the `otpauth` scheme. It was [initially introduced by Google Authenticator](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) but is now universally accepted by all authenticator apps.
### How do I check _when_ a user went through MFA?
Access tokens issued by Supabase Auth contain an `amr` (Authentication Methods
Reference) claim. It is an array of objects that indicate what authentication
methods the user has used so far.
Access tokens issued by Supabase Auth contain an `amr` (Authentication Methods Reference) claim. It is an array of objects that indicate what authentication methods the user has used so far.
For example, the following structure describes a user that first signed in with
a password-based method, and then went through TOTP MFA 2 minutes and 12
seconds later. The entries are ordered most recent method first!
For example, the following structure describes a user that first signed in with a password-based method, and then went through TOTP MFA 2 minutes and 12 seconds later. The entries are ordered most recent method first!
```json
{
@@ -670,8 +600,7 @@ seconds later. The entries are ordered most recent method first!
}
```
Use the `supabase.auth.getAuthenticatorAssuranceLevel()` method to get easy
access to this information in your browser app.
Use the `supabase.auth.getAuthenticatorAssuranceLevel()` method to get easy access to this information in your browser app.
You can use this PostgreSQL snippet in RLS policies, too:
@@ -686,8 +615,7 @@ json_query_path(auth.jwt(), '$.amr[0]')
- `$.amr[0]` is a SQL/JSON path expression that fetches the most recent
authentication method in the JWT.
Once you have extracted the most recent entry in the array, you can compare the
`method` and `timestamp` to enforce stricter rules. For instance, you can mandate that access will be only be granted on a table to users who have recently signed in with a password.
Once you have extracted the most recent entry in the array, you can compare the `method` and `timestamp` to enforce stricter rules. For instance, you can mandate that access will be only be granted on a table to users who have recently signed in with a password.
Currently recognized authentication methods are:
+165 -355
View File
@@ -3,13 +3,14 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'row-level-security',
title: 'Row Level Security',
description: 'Secure your data using Postgres Row Level Security.',
video: 'https://www.youtube.com/v/Ow_Uzedfohk',
description: 'Using Row Level Security with Supabase Auth.',
subtitle: 'Using Row Level Security with Supabase Auth.',
tocVideo: 'Ow_Uzedfohk',
}
When you need granular authorization rules, nothing beats PostgreSQL's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html).
Supabase Auth is designed to work perfectly with [Postgres Row Level Security](/docs/guides/database/postgres/row-level-security) (RLS).
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are PostgreSQL's rule engine. They are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
You can use RLS to create [Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) that are incredibly powerful and flexible, allowing you to write complex SQL rules which fit your unique business needs.
<div className="video-container">
<iframe
@@ -27,8 +28,8 @@ You can just think of them as adding a `WHERE` clause to every query. For exampl
```sql
create policy "Individuals can view their own todos."
on todos for select
using ( auth.uid() = user_id );
on todos for select
using ( auth.uid() = user_id );
```
.. would translate to this whenever a user tries to select from the todos table:
@@ -39,9 +40,33 @@ from todos
where auth.uid() = todos.user_id; -- Policy is implicitly added.
```
## Authenticated and anonymous roles
Supabase Auth maps every request to one of the roles:
- `anon`: an anonymous request (the user is not logged in)
- `authenticated`: an authenticated request (the user is logged in)
These are actually [Postgres Roles](/docs/guides/database/postgres/roles), and so they have significant value for the [performance](/docs/guides/database/postgres/row-level-security#specify-roles-in-your-policies) of your RLS Policies. 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 );
```
## Helper Functions
Supabase provides you with a few easy functions that you can use with your policies.
Supabase provides some helper functions that make it easier to write Policies.
### `auth.uid()`
@@ -49,80 +74,117 @@ Returns the ID of the user making the request.
### `auth.jwt()`
Returns the JWT of the user making the request.
Returns the JWT of the user making the request. Anything that you store in the user's `app_metadata` column or the `user_metadata` column will be accessible using this function. It's important to know the distinction between these two:
## Examples
- `user_metadata` - can be updated by the authenticated user using the `supabase.auth.update()` function. It is not a good place to store authorization data.
- `app_metadata` - cannot be updated by the user, so it's a good place to store authorization data.
Here are some examples to show you the power of PostgreSQL's RLS.
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:
### Allow read access
```sql
-- 1. Create table
create table profiles (
id uuid references auth.users,
avatar_url text
);
-- 2. Enable RLS
alter table profiles
enable row level security;
-- 3. Create Policy
create policy "Public profiles are viewable by everyone."
on profiles for select using (
true
);
```sql
create policy "User is in team"
on my_table
to authenticated
using ( team_id in (select auth.jwt() -> 'app_metadata' -> 'teams'));
```
1. Creates a table called `profiles` in the public schema (default schema).
2. Enables Row Level Security.
3. Creates a policy which allows all `select` queries to run.
{/* prettier-ignore */}
<Admonition type="caution">
Keep in mind that a JWT is not always "fresh". In the example above, 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.
### Restrict updates
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>
## Important considerations
We recommend reading the [Row Level Security](/docs/guides/database/postgres/row-level-security) guide in the database section to learn more about Postgres RLS. When using Postgres on Supabase there are some important things to keep in mind to maintain data security.
### Never use a service key on the client
Supabase provides special "Service" keys, which can be used to bypass RLS. These should never be used in the browser or exposed to customers, but they are useful for administrative tasks.
<Admonition type="note">
Supabase will adhere to the RLS policy of the signed-in user, even if the client library is initialized with a Service Key.
</Admonition>
### Always enable RLS on public tables
You should _always_ enable RLS on tables created in a public schema. This is considered "default safe". Unfortunately this is not enabled by default on Postgres, so you will need to keep this in mind - especially if you are using the SQL Editor or database migrations. RLS is already enabled by default if you create a table using the Table Editor. If you want to allow public access to a table, just add a Policy with `true`:
```sql
create policy "Allow public access"
on my_table for select
using ( true );
```
### Using external authorization systems
If you want to use another authorization method for your applications that's also fine. Supabase is "just Postgres", so if your application works with Postgres, then it also works with Supabase. If you take this path, don't put your tables in the `public` schema - instead create a new schema for your tables and functions:
```sql
create schema private;
create table private.employees (
id serial primary key,
name text
);
```
If you do put anything in the `public` schema, make sure to enable RLS (you don't need to add any policies):
```sql
-- 1. Create table
create table profiles (
id uuid references auth.users,
avatar_url text
id serial primary key,
email text
);
-- 2. Enable RLS
alter table profiles
enable row level security;
-- 3. Create Policy
create policy "Users can update their own profiles."
on profiles for update using (
auth.uid() = id
);
alter table profiles enable row level security;
```
This makes the tables inaccessible via the [APIs](/docs/guides/api).
## Usage
Row Level Security is extremely versatile, since it simply uses SQL to express access rules for your data.
### Using functions
You can use any Postgres function inside a Policy. The [Helper Functions](#helper-functions) above are simply Postgres functions we've made available in the `auth` schema. This is an example which:
1. Creates a table called `profiles` in the public schema (default schema).
2. Enables RLS.
3. Creates a policy which allows logged in users to update their own data.
**Note:** If you want to use upsert operations, the user needs to have `INSERT`, `UPDATE`, and `SELECT` permissions.
### Only anon or authenticated access
You can add a Postgres role
3. Creates a policy which allows logged in users to update their own data, using the `auth.uid()` function.
```sql
create policy "Public profiles are viewable by everyone."
on profiles for select
to authenticated, anon
-- 1. Create table
create table profiles (
id uuid 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 profiles"
on profiles for update
to authenticated
using (
true
auth.uid() = id
);
```
### Policies with joins
**Note:** If you want to use [upsert](/docs/reference/javascript/upsert) operations, the user needs to have `INSERT`, `UPDATE`, and `SELECT` permissions.
Policies can even include table joins. This example shows how you can query "external" tables to build more advanced rules.
### Using joins
Policies can include table joins. This example shows how you can query "external" tables to build more advanced rules. RLS policies are executed on every access of the table, so be careful to make sure that policies are efficient.
```sql
-- 1. Create a table of teams
create table teams (
id serial primary key,
name text
@@ -135,11 +197,10 @@ create table members (
);
-- 3. Enable RLS
alter table teams
enable row level security;
alter table teams enable row level security;
-- 4. Create Policy
create policy "Team members can update team details if they belong to the team."
create policy "Team members can update team details if they belong to the team"
on teams
for update using (
auth.uid() in (
@@ -149,331 +210,80 @@ create policy "Team members can update team details if they belong to the team."
);
```
**Note:** If RLS is also enabled for _members_, the user must also have read (_select_) access to _members_. Otherwise the joined query will not yield any results.
An important note here: if RLS is also enabled for `members`, the user must also have read (`select`) access to members. Otherwise the joined query will not yield any results. Another alternative is to use a "security definer" function which is created by a user with [bypassrls](/docs/guides/database/postgres/row-level-security#specify-roles-in-your-policies) privileges.
### Policies with security definer functions
### Using security definer functions
Policies can also make use of `security definer functions`. This is useful in a many-to-many relationship where you want to restrict access to the linking table. Following the `teams` and `members` example from above, this example shows how you can use the security definer function in combination with a policy to control access to the `members` table.
You can use `security definer` functions inside Policies. This is useful in a many-to-many relationships, and [important for performance](/docs/guides/database/postgres/row-level-security#use-security-definer-functions). Following the `teams` and `members` example from above, this example shows how you can use the security definer function in combination with a policy to control access to the `members` table.
```sql
-- 1. Follow example for 'Policies with joins' above
-- 1. Create a table of teams
create table teams (
id serial primary key,
name text
);
-- 2. Create many to many join
create table members (
team_id bigint references teams,
user_id uuid references auth.users
);
-- 2. Enable RLS
alter table members
enable row level security
alter table teams enable row level security;
alter table members enable row level security
-- 3. Create security definer function
create or replace function get_teams_for_authenticated_user()
-- 3. Create security definer function, which should be run as "postgres"
create function private.get_teams_for_authenticated_user()
returns setof bigint
language sql
security definer
set search_path = public
stable
as $$
select team_id
from members
where user_id = auth.uid()
select team_id
from members
where user_id = auth.uid()
$$;
-- 4. Create Policy
create policy "Team members can update team members if they belong to the team."
on members
for all using (
team_id in (
select get_teams_for_authenticated_user()
)
);
```
### Verifying email domains
Postgres has a function `right(string, n)` that returns the rightmost n characters of a string.
You could use this to match staff member's email domains.
```sql
-- 1. Create table
create table leaderboard (
id uuid references auth.users,
high_score bigint
);
-- 2. Enable RLS
alter table leaderboard
enable row level security;
-- 3. Create Policy
create policy "Only Blizzard staff can update leaderboard"
on leaderboard
for update using (
right(auth.jwt() ->> 'email', 13) = '@blizzard.com'
);
```
### Time to live for rows
Policies can also be used to implement TTL or time to live feature that you see in Instagram stories or Snapchat.
In the following example, rows of `stories` table are available only if they have been created within the last 24 hours.
```sql
-- 1. Create table
create table if not exists stories (
id uuid not null primary key DEFAULT uuid_generate_v4(),
created_at timestamp with time zone default timezone('utc' :: text, now()) not null,
content text not null
);
-- 2. Enable RLS
alter table stories
enable row level security;
-- 3. Create Policy
create policy "Stories are live for a day"
on stories
for select using (
created_at > (current_timestamp - interval '1 day')
);
```
### Advanced policies
Use the full power of SQL to build extremely advanced rules.
In this example, we will create a `posts` and `comments` tables and then create a policy that depends on another policy.
(In this case, the comments policy depends on the posts policy.)
```sql
create table posts (
id serial primary key,
creator_id uuid not null references auth.users(id),
title text not null,
body text not null,
publish_date date not null default now(),
audience uuid[] null -- many to many table omitted for brevity
);
create table comments (
id serial primary key,
post_id int not null references posts(id) on delete cascade,
user_id uuid not null references auth.users(id),
body text not null,
comment_date date not null default now()
);
create policy "Creator can see their own posts"
on posts
for select
using (
auth.uid() = posts.creator_id
);
create policy "Logged in users can see the posts if they belong to the post 'audience'."
on posts
for select
using (
auth.uid() = any (posts.audience)
);
create policy "Users can see all comments for posts they have access to."
on comments
for select
using (
exists (
select 1 from posts
where posts.id = comments.post_id
on members
for all using (
team_id in (
select private.get_teams_for_authenticated_user()
)
);
```
## Tips
### Using built-in functions
### You don't have to use policies
Postgres has a number of built-in functions. Most commonly you'll use `in()` and `any()` which will match a column's value to a list of values.
You can also put your authorization rules in your middleware, similar to how you would create security rules with any other `backend <-> middleware <-> frontend` architecture. You can use Edge Functions to run this architecture, or you can use your favorite server framework, like Rails, Django, Node.js, Phoenix, or Laravel.
Policies are a tool. In the case of "serverless/Jamstack" setups, they are especially effective because you don't have to deploy any middleware at all and can use the javascript libraries directly from the browser.
That said, if you want to use another authorization method for your applications that's also fine. Supabase is "just Postgres", so if your application works with Postgres, then it also works with Supabase.
If you plan to use this approach make sure to enable RLS for your tables. Then use the `service_role` key (for our client libraries) or the `postgres` role - both of these can bypass RLS. You don't need to create any policies with this approach, simply enabling RLS is sufficient:
You can use any Postgres functions inside Policies. For example, we can use the `right(string, n)` function to match email domains:
```sql
create table profiles (
id serial primary key,
email text
);
alter table profiles enable row level security;
```
### Never use a service key on the client
Supabase provides special "Service" keys, which can be used to bypass all RLS.
These should never be used in the browser or exposed to customers, but they are useful for administrative tasks.
<Admonition type="caution">
Initializing a client with a Service Key will not override RLS if a user token is set. If a user is signed in with a client, Supabase will adhere to the RLS policy of the user.
</Admonition>
### Testing policies
To test policies on the database itself (i.e., from the [SQL Editor](https://supabase.com/dashboard/project/_/sql) or from `psql`) without switching to your frontend and logging in as different users, you can utilize the following helper SQL procedures ([credits](https://github.com/supabase/supabase/issues/7311#issuecomment-1398648114)):
```sql
grant anon, authenticated to postgres;
create or replace procedure auth.login_as_user (user_email text)
language plpgsql
as $$
declare
auth_user auth.users;
begin
select
* into auth_user
from
auth.users
where
email = user_email;
execute format('set request.jwt.claim.sub=%L', (auth_user).id::text);
execute format('set request.jwt.claim.role=%I', (auth_user).role);
execute format('set request.jwt.claim.email=%L', (auth_user).email);
execute format('set request.jwt.claims=%L', json_strip_nulls(json_build_object('app_metadata', (auth_user).raw_app_meta_data))::text);
raise notice '%', format( 'set role %I; -- logging in as %L (%L)', (auth_user).role, (auth_user).id, (auth_user).email);
execute format('set role %I', (auth_user).role);
end;
$$;
create or replace procedure auth.login_as_anon ()
language plpgsql
as $$
begin
set request.jwt.claim.sub='';
set request.jwt.claim.role='';
set request.jwt.claim.email='';
set request.jwt.claims='';
set role anon;
end;
$$;
create or replace procedure auth.logout ()
language plpgsql
as $$
begin
set request.jwt.claim.sub='';
set request.jwt.claim.role='';
set request.jwt.claim.email='';
set request.jwt.claims='';
set role postgres;
end;
$$;
```
To switch to a given user (by email), use `call auth.login_as_user('my@email.com');`. You can also switch to the `anon` role using `call auth.login_as_anon();`. When you are done, use `call auth.logout();` to return yourself to the `postgres` role.
These procedures can also be used for writing [pgTAP](/docs/guides/database/extensions/pgtap) unit tests for policies.
<details>
<summary>Click here to see an example `psql` interaction using this.</summary>
This example shows that the `public.profiles` table from the tutorial example can indeed be updated by the `postgres` role and the owner of the row but not from `anon` connections:
```shell
postgres=> select id, email from auth.users;
id | email
--------------------------------------+-------------------
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | user1@example.com
15d6811a-16ee-4fa2-9b18-b63085688be4 | user2@example.com
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | user3@example.com
(3 rows)
postgres=> table public.profiles;
id | updated_at | username | full_name | avatar_url | website
--------------------------------------+------------+----------+-----------+------------+---------
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | | user1 | User 1 | |
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user2 | User 2 | |
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user3 | User 3 | |
(3 rows)
postgres=> call auth.login_as_anon();
CALL
postgres=> update public.profiles set updated_at=now();
UPDATE 0 -- anon users cannot update any profile but see all of them
postgres=> table public.profiles;
id | updated_at | username | full_name | avatar_url | website
--------------------------------------+------------+----------+-----------+------------+---------
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | | user1 | User 1 | |
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user2 | User 2 | |
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user3 | User 3 | |
(3 rows)
postgres=> call auth.logout();
CALL
postgres=> call auth.login_as_user('user1@example.com');
NOTICE: set role authenticated; -- logging in as 'd4f0aa86-e6f6-41d1-bd32-391f077cf1b9' ('user1@example.com')
CALL
postgres=> update public.profiles set updated_at=now();
UPDATE 1 -- authenticated users can update their own profile and see all of them
postgres=> table public.profiles;
id | updated_at | username | full_name | avatar_url | website
--------------------------------------+-------------------------------+----------+-----------+------------+---------
15d6811a-16ee-4fa2-9b18-b63085688be4 | | user1 | User 1 | |
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | | user2 | User 2 | |
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:39:16.204612+00 | user3 | User 3 | |
(3 rows)
postgres=> call auth.logout();
CALL
postgres=> update public.profiles set updated_at=now();
UPDATE 3 -- the 'postgres' role can update and see all profiles
postgres=> table public.profiles;
id | updated_at | username | full_name | avatar_url | website
--------------------------------------+-------------------------------+----------+-----------+------------+---------
15d6811a-16ee-4fa2-9b18-b63085688be4 | 2023-02-18 21:40:08.216324+00 | user1 | User 1 | |
4e1010bb-eb37-4a4d-a05a-b0ee315c9d56 | 2023-02-18 21:40:08.216324+00 | user2 | User 2 | |
d4f0aa86-e6f6-41d1-bd32-391f077cf1b9 | 2023-02-18 21:40:08.216324+00 | user3 | User 3 | |
(3 rows)
```
</details>
## Deprecated features
We have deprecate some functions to ensure better performance and extensibilty of RLS policies.
### `auth.role()`
<Admonition type="caution">
Deprecated: The `auth.role()` function has been deprecated in favour of using the `TO` field, natively supported within Postgres.
</Admonition>
```sql
-- DEPRECATED
create policy "Public profiles are viewable by everyone."
on profiles for select using (
auth.role() = 'authenticated' or auth.role() = 'anon'
);
-- RECOMMENDED
create policy "Public profiles are viewable by everyone."
on profiles for select
to authenticated, anon
using (
true
create policy "Only Supabase staff can update the leaderboard"
on leaderboard
to authenticated
for update using (
right(auth.jwt() ->> 'email', 13) = '@supabase.com'
);
```
### `auth.email()`
### Using Multi-factor Authentication
<Admonition type="caution">
RLS can be [combined with Multi-Factor Authentication](/docs/guides/auth/auth-mfa#enforce-rules-for-mfa-logins) in Supabase Auth. For example, you could restrict a user from updating their profile unless they have at least 2 levels of authentication (Assurance Level 2):
Deprecated. Use `auth.jwt() ->> 'email'` instead.
</Admonition>
Returns the email of the user making the request.
```sql
create policy "Restrict updates."
on profiles
as restrictive
for update
to authenticated using (
auth.jwt()->>'aal' = 'aal2'
);
```
export const Page = ({ children }) => <Layout meta={meta} children={children} />
+11 -19
View File
@@ -3,25 +3,24 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'database',
title: 'Database',
description: 'Use Supabase to manage your data.',
description: 'Using Supabase to manage your data.',
subtitle: 'Using Supabase to manage your data.',
sidebar_label: 'Overview',
}
Every Supabase project comes with a full [Postgres](https://www.postgresql.org/) database, a free and open source
database which is considered one of the world's most stable and advanced databases.
Every Supabase project comes with a full [Postgres](https://www.postgresql.org/) database, a free and open source database which is considered one of the world's most stable and advanced databases.
## Postgres or PostgreSQL?
PostgreSQL the database was derived from the POSTGRES Project, a package written at the University of California at Berkeley in 1986.
This package included a query language called "PostQUEL".
PostgreSQL the database was derived from the POSTGRES Project, a package written at the University of California at Berkeley in 1986. This package included a query language called "PostQUEL".
In 1994, Postgres95 was built on top of POSTGRES code, adding an SQL language interpreter as a replacement for PostQUEL.
Eventually, Postgres95 was renamed to PostgreSQL to reflect the SQL query capability.
In 1994, Postgres95 was built on top of POSTGRES code, adding an SQL language interpreter as a replacement for PostQUEL. Eventually, Postgres95 was renamed to PostgreSQL to reflect the SQL query capability.
After this, many people referred to it as Postgres since it's less prone to confusion. Supabase is all about
simplicity, so we also refer to it as Postgres.
After this, many people referred to it as Postgres since it's less prone to confusion. Supabase is all about simplicity, so we also refer to it as Postgres.
## Features
## Supabase Studio
Postgres is known to be one of the most extensible databases available, so you can use it to build almost any application. The Supabase platform makes this easier with a simple-to-use dashboard called "Supabase Studio".
### Table View
@@ -71,16 +70,13 @@ Supabase comes with a SQL Editor. You can also save your favorite queries to run
<Admonition type="note">
Database backups **do not** include objects stored via the Storage API, as the database only
includes metadata about these objects. Restoring an old backup does not restore objects that have
been deleted since then.
Database backups **do not** include objects stored via the Storage API, as the database only includes metadata about these objects. Restoring an old backup does not restore objects that have been deleted since then.
</Admonition>
### Extensions
To expand the functionality of your Postgres database, you can use extensions.
You can enable Postgres extensions with the click of a button within the Supabase dashboard.
To expand the functionality of your Postgres database, you can use extensions. You can enable Postgres extensions with the click of a button within the Supabase dashboard.
<video width="99%" muted playsInline controls={true}>
<source
@@ -91,10 +87,6 @@ You can enable Postgres extensions with the click of a button within the Supabas
[Learn more](/docs/guides/database/extensions) about all the extensions provided on Supabase.
## Tips
Read about resetting your database password [here](/docs/guides/database/managing-passwords) and changing the timezone of your server [here](/docs/guides/database/managing-timezones).
## Next steps
- Read more about [Postgres](https://www.postgresql.org/about/)
@@ -4,7 +4,8 @@ export const meta = {
id: 'full-text-search',
title: 'Full Text Search',
description: 'How to use full text search in PostgreSQL.',
video: 'https://www.youtube.com/v/b-mgca_2Oe4',
subtitle: 'How to use full text search in PostgreSQL.',
tocVideo: 'b-mgca_2Oe4',
}
Postgres has built-in functions to handle `Full Text Search` queries. This is like a "search engine" within Postgres.
+1 -1
View File
@@ -2,7 +2,7 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'json',
title: 'Analyzing efficiency and performance from the terminal',
title: 'Debugging and monitoring',
description:
'Inspecting your Postgres database for common issues around disk, query performance, index, locks, and more using the terminal.',
}
@@ -6,7 +6,7 @@ export const meta = {
description: 'How to change your PostgreSQL database password.',
}
Your PostgreSQL database is the core of your Supabase project, so it's important that it has a strong, secure password at all times.
Your Postgres database is the core of your Supabase project, so it's important that it has a strong, secure password at all times.
If you use special symbols in your postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the postgres connection string e.g. `postgresql://postgres:p%3Dword@db.cvwawazfelidkloqmbma.supabase.co:5432/postgres`
@@ -0,0 +1,110 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'postgres-configuration',
title: 'Database configuration',
slug: 'postgres-configuration',
description: 'Updating the default congfiguration for your Postgres database.',
subtitle: 'Updating the default congfiguration for your Postgres database.',
}
Postgres provides a set of sensible defaults for you database size. In some cases, these defaults can be updated. We do not recommend changing these defaults unless you know what you're doing.
## Timeouts
By default, Supabase limits the maximum statement execution time to _8 seconds_ for users accessing the API. Additionally, all users are subject to a global limit of _2 minutes_. This serves as a backstop against resource exhaustion due to either poorly written queries, or abusive usage.
### Changing the default timeout
The timeout values were picked as a reasonable default for the majority of use-cases, but can be modified using the [`alter role`](https://www.postgresql.org/docs/current/sql-alterrole.html) statement:
```sql
-- For API users
alter role authenticator set statement_timeout = '15s';
```
You can also update the statement timeout for a session:
```sql
set statement_timeout to 60000; -- 1 minute in milliseconds
```
### Statement Optimization
All Supabase projects come with the [`pg_stat_statements`](https://www.postgresql.org/docs/current/pgstatstatements.html) extension installed, which tracks planning and execution statistics for all statements executed against it. These statistics can be used in order to diagnose the performance of your project.
This data can further be used in conjunction with the [`explain`](https://www.postgresql.org/docs/current/using-explain.html) functionality of Postgres to optimize your usage.
## Managing Timezones
Every Supabase database is set to UTC timezone by default. We strongly recommend keeping it this way, even if your users are in a different location.
This is because it makes it much easier to calculate differences between timezones if you adopt the mental model that "everything in my database is in UTC time".
### Change timezone
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="sql"
>
<TabPanel id="sql" label="SQL">
```sql
alter database postgres
set timezone to 'America/New_York';
```
</TabPanel>
</Tabs>
### Full list of timezones
Get a full list of timezones supported by your database. This will return the following columns:
- `name`: Time zone name
- `abbrev`: Time zone abbreviation
- `utc_offset`: Offset from UTC (positive means east of Greenwich)
- `is_dst`: True if currently observing daylight savings
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="sql"
>
<TabPanel id="sql" label="SQL">
```sql
select name, abbrev, utc_offset, is_dst
from pg_timezone_names()
order by name;
```
</TabPanel>
</Tabs>
### Search for a specific timezone
Use `ilike` (case insensitive search) to find specific timezones.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="sql"
>
<TabPanel id="sql" label="SQL">
```sql
select *
from pg_timezone_names()
where name ilike '%york%';
```
</TabPanel>
</Tabs>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,134 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'postgres-roles',
title: 'Postgres Roles',
description: 'Managing access to your Postgres database and configuring permissions.',
subtitle: 'Managing access to your Postgres database and configuring permissions.',
}
Postgres manages database access permissions using the concept of roles. Generally you wouldn't use these roles for your own application - they are mostly for configuring _system access_ to your database. If you want to confgure _application access_, then you should use [Row Level Security](/docs/guides/database/postgres/row-level-security).
## Users vs Roles
In PostgreSQL, roles can function as users or groups of users. Users are roles with login privileges, while groups (also known as role groups) are roles that don't have login privileges but can be used to manage permissions for multiple users.
## Superuser roles
The superuser role has unrestricted access to the database system. Typically these roles will be able to bypass all security measures (like Row Level Security). Be cautious when granting superuser privileges as it can potentially lead to security risks.
## Creating roles
You can create a role using the `create role` command:
```sql
create role "role_name";
```
## Creating users
Roles and users are essentially the same in Postgres, however if you want to use password-logins for a specific role, then you can use `WITH LOGIN PASSWORD`:
```sql
create role "role_name" with login 'extremely_secure_password';
```
## Passwords
Your Postgres database is the core of your Supabase project, so it's important that every role has a strong, secure password at all times. Here are some tips for creating a secure password:
- Use a password manager to generate it.
- Make a long password (12 characters at least).
- Don't use any common dictionary words.
- Use both upper and lower case characters, numbers, and special symbols.
### Special symbols in passwords
If you use special symbols in your Postgres password, you must remember to [percent-encode](https://en.wikipedia.org/wiki/Percent-encoding) your password later if using the Postgres connection string e.g. `postgresql://postgres:p%3Dword@db.xxx.supabase.co:5432/postgres`.
### Changing your project password
When you created your project you were also asked to enter a password. This is actually the password for the `postgres` role in your database. You can update this from the Dashboard under the [database settings](https://supabase.com/dashboard/project/_/settings/database) page. You should _never_ give this to 3rd-party service unless you absolutely trust them. Instead, we recommend that you create a new user for every service that you want to give access too. This will also help you with debugging - you can see every query that each role is executing in your database within `pg_stat_statements`.
## Granting Permissions
Roles can be granted various permissions on database objects using the `GRANT` command. Permissions include `SELECT`, `INSERT`, `UPDATE`, and `DELETE`. You can configure access to almost any object inside your database - including tables, views, functions, and triggers.
## Revoking Permissions
Permissions can be revoked using the `REVOKE` command:
```sql
REVOKE permission_type ON object_name FROM role_name;
```
## Role Hierarchy
Roles can be organized in a hierarchy, where one role can inherit permissions from another. This simplifies permission management, as you can define permissions at a higher level and have them automatically apply to all child roles.
### Role inheritance
To create a role hierarchy, you first need to create the parent and child roles. The child role will inherit permissions from its parent. Child roles can be added using the INHERIT option when creating the role:
```sql
create role "child_role_name" inherit "parent_role_name";
```
### Preventing inheritance
In some cases, you might want to prevent a role from having a child relationship (typically superuser roles). You can prevent inheritance relations using NOINHERIT:
```sql
alter role "child_role_name" noinherit;
```
## Supabase Roles
Postgres comes with a set of [predefined roles](https://www.postgresql.org/docs/current/predefined-roles.html). Supabase extends this with a default set of roles which are configured on your database when you start a new project:
### `postgres`
The default Postgres role. This has admin privileges.
### `anon`
For "anonymous access". This is the role which the API (PostgREST) will use when a user _is not_ logged in.
### `authenticator`
A special role for the API (PostgREST). It has very limited access, and is used to validate a JWT and then
"change into" another role determined by the JWT verification.
### `authenticated`
For "authenticated access". This is the role which the API (PostgREST) will use when a user _is_ logged in.
### `service_role`
For elevated access. This role is used by the API (PostgREST) to bypass Row Level Security.
### `supabase_auth_admin`
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `auth` schema.
### `supabase_storage_admin`
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `storage` schema.
### `dashboard_user`
For running commands via the Supabase UI.
### `supabase_admin`
Supabase Administrative role for maintaining your database.
## Resources
- Official Postgres docs: [Database Roles](https://www.postgresql.org/docs/current/database-roles.html)
- Official Postgres docs: [Role Membership](https://www.postgresql.org/docs/current/role-membership.html)
- Official Postgres docs: [Function Permissions](https://www.postgresql.org/docs/current/perm-functions.html)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,380 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
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.',
}
When you need granular authorization rules, nothing beats Postgres's [Row Level Security (RLS)](https://www.postgresql.org/docs/current/ddl-rowsecurity.html).
## Row Level Security in Supabase
RLS is incredibly powerful and flexible, allowing you to write complex SQL rules that fit your unique business needs. RLS can be [combined with Supabase Auth](/docs/guides/auth/row-level-security) for end-to-end user security from the browser to the database.
RLS is a Postgres primitive and can provide "[defense in depth](<https://en.wikipedia.org/wiki/Defense_in_depth_(computing)>)" to protect your data from malicious actors even when accessed through 3rd party tooling.
## Policies
[Policies](https://www.postgresql.org/docs/current/sql-createpolicy.html) are Postgres's rule engine. Policies are easy to understand once you get the hang of them. Each policy is attached to a table, and the policy is executed every time a table is accessed.
You can just think of them as adding a `WHERE` clause to every query. For example a policy like this ...
```sql
create policy "Individuals can view their own todos."
on todos for select
using ( auth.uid() = user_id );
```
.. would translate to this whenever a user tries to select from the todos table:
```sql
select *
from todos
where auth.uid() = todos.user_id;
-- Policy is implicitly added.
```
## Enabling Row Level Security
You can enable RLS for any table using the `enable row level security` clause:
```sql
alter table "table_name" enable row level security;
```
Once you have enabled RLS, no data will be accessible until you create policies.
## Creating Policies
Policies are simply SQL logic that you attach to a Postgres table. You can attach as many policies as you want to each table.
Supabase provides some [helpers](/docs/guides/auth/row-level-security#helper-functions) that simplify RLS if you're using Supabase Auth. We'll use these helpers to illustrate some basic policies:
### SELECT Policies
You can specify select policies with the `using` clause.
Let's say you have a table called `profiles` in the public schema and you want enable read access to everyone.
```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 "Public profiles are visible to everyone."
on profiles for select
to anon -- the Postgres Role (recommended)
using ( true ); -- the actual Policy
```
Alternatively, if you only wanted users to be able to see their own profiles:
```sql
create policy "Profiles are viewable by everyone."
on profiles
for select using ( auth.uid() = user_id );
```
### INSERT Policies
You can specify select policies with the `with check` clause.
Let's say you have a table called `profiles` in the public schema and you only want users to be able to create a profile for themselves. In that case, we want to check their User ID matches the value that they are trying to insert:
```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 create a profile."
on profiles for insert
to authenticated -- the Postgres Role (recommended)
using ( auth.id() = user_id ); -- the actual Policy
```
### UPDATE Policies
You can specify select policies with the `using` clause.
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` clause.
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" privilege:
```sql
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 privilege.
## RLS Performance Recommendations
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):
{/* prettier-ignore */}
```js
const { data } = supabase
.from('table')
.select()
```
You should always add a filter:
{/* prettier-ignore */}
```js
const { data } = supabase
.from('table')
.select()
.eq('user_id', userId)
```
Even though this duplicates the contents of the Policy, Postgres can use the to construct a better query plan.
#### Benchmarks
| Test | Before (ms) | After (ms) | % Improvement | Change |
| ------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| [test3-addfilter](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test3-addfilter) | 171 | 9 | 94.74% | <details className="cursor-pointer">Before:<br/>`auth.uid() = user_id`<br/><br/>After:<br/>add `.eq` or `where` on `user_id`</details> |
### 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 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>
### Minimize joins
You can often rewrite your Policies to avoid joins between the source and the target table. Instead, try to organize your policy to fetch all the relevant data from the target table into an array or set, then you can use an `IN` or `ANY` operation in your filter.
For example, this is an example of a slow policy which joins the source `test_table` to the target `team_user`:
```sql
create policy "rls_test_select" on test_table
to authenticated
using (
auth.uid() in (
select user_id
from team_user
where team_user.team_id = team_id -- joins to the source "test_table.team_id"
)
);
```
We can rewrite this to avoid this join, and instead select the filter criteria into a set:
```sql
create policy "rls_test_select" on test_table
to authenticated
using (
team_id in (
select team_id
from team_user
where user_id = auth.uid() -- no join
)
);
```
In this case you can also consider [using a `security definer` function](#use-security-definer-functions) to bypass RLS on the join table:
{/* prettier-ignore */}
<Admonition type="note">
If the list exceeds 1000 items, a different approach may be needed or you may need to analyze the approach to ensure that the performance is acceptable.
</Admonition>
#### Benchmarks
| Test | Before (ms) | After (ms) | % Improvement | Change |
| --------------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| [test5-fixed-join](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test5-fixed-join) | 9,000 | 20 | 99.78% | <details className="cursor-pointer">Before:<br/>`auth.uid()` in table join on col<br/><br/>After:<br/>col in table join on `auth.uid()`</details> |
### Specify Roles in your Policies
Always use the Role of inside your policies, specified by the `TO` operator. For example, instead of this query:
```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 ( auth.uid() = user_id );
```
This prevents the policy `( auth.uid() = user_id )` from running for any `anon` users, since the execution stops at the `to authenticated` step.
#### Benchmarks
| Test | Before (ms) | After (ms) | % Improvement | Change |
| --------------------------------------------------------------------------------------------- | ----------- | ---------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [test6-To-role](https://github.com/GaryAustin1/RLS-Performance/tree/main/tests/test6-To-role) | 170 | < 0.1 | 99.78% | <details className="cursor-pointer">Before:<br/>No `TO` policy<br/><br/>After:<br/>`TO authenticated` (anon accessing)</details> |
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -0,0 +1,122 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'postgres-triggers',
title: 'Postgres Triggers',
description: 'Automatically execute SQL on table events.',
subtitle: 'Automatically execute SQL on table events.',
}
In Postgres, a trigger executes a set of actions automatically on table events such as INSERTs, UPDATEs, DELETEs, or TRUNCATE operations.
## Creating a Trigger
Creating triggers involve 2 parts:
1. A [Function](docs/guides/database/functions) which will be executed (called the Trigger Function)
2. The actual Trigger object, with parameters around when the trigger should be run.
An example of a trigger is:
```sql
create trigger "trigger_name"
after insert on "table_name"
for each row
execute statement trigger_function();
```
## Trigger Functions
A trigger function is a user-defined [Function](docs/guides/database/functions) that Postgres executes when the trigger is fired.
### Example trigger function
Here is an example that updates `salary_log` whenever an employee's salary is updated:
```sql
-- Example: Update salary_log when salary is updated
create function update_salary_log()
returns trigger
language plpgsql
as $$
begin
insert into salary_log(employee_id, old_salary, new_salary)
values (new.id, old.salary, new.salary);
return new;
end;
$$;
create trigger salary_update_trigger
after update on employees
for each row
exectute function update_salary_log();
```
### Trigger variables
Trigger functions have access to several special variables that provide information about the context of the trigger event and the data being modified. In the example above you can see the values inserted into the salary log are `old.salary` and `new.salary` - in this case `old` specifies the previous values and `new` specifies the updated values.
Here are some of the key variables and options available within trigger functions:
- `TG_NAME`: The name of the trigger being fired.
- `TG_WHEN`: The timing of the trigger event (`BEFORE` or `AFTER`).
- `TG_OP`: The operation that triggered the event (`INSERT`, `UPDATE`, `DELETE`, or `TRUNCATE`).
- `OLD`: A record variable holding the old row's data in `UPDATE` and `DELETE` triggers.
- `NEW`: A record variable holding the new row's data in `UPDATE` and `INSERT` triggers.
- `TG_LEVEL`: The trigger level (`ROW` or `STATEMENT`), indicating whether the trigger is row-level or statement-level.
- `TG_RELID`: The object ID of the table on which the trigger is being fired.
- `TG_TABLE_NAME`: The name of the table on which the trigger is being fired.
- `TG_TABLE_SCHEMA`: The schema of the table on which the trigger is being fired.
- `TG_ARGV`: An array of string arguments provided when creating the trigger.
- `TG_NARGS`: The number of arguments in the `TG_ARGV` array.
## Types of Triggers
There are two types of trigger, `BEFORE` and `AFTER`:
### Trigger before changes are made
Executes before the triggering event.
```sql
create trigger before_insert_trigger
before insert on orders
for each row
execute statement before_insert_function();
```
### Trigger after changed are made
Executes after the triggering event.
```sql
create trigger after_delete_trigger
after delete on customers
for each row
execute statement after_delete_function();
```
## Execution frequency
There are two options available for executing triggers:
- `for each row`: specifies that the trigger function should be executed once for each affected row.
- `for each statement`: the trigger is executed once for the entire operation (eg, once on insert). This can be more efficient than `for each row` when dealing with multiple rows affected by a single SQL statement, as they allow you to perform calculations or updates on groups of rows at once.
## Dropping a Trigger
You can delete a trigger using the `drop trigger` command:
```sql
drop trigger "trigger_name" on "table_name";
```
## Resources
- Official Postgres Docs: [Triggers](https://www.postgresql.org/docs/current/triggers.html)
- Official Postgres Docs: [Overview of Trigger Behavior](https://www.postgresql.org/docs/current/trigger-definition.html)
- Official Postgres Docs: [CREATE TRIGGER](https://www.postgresql.org/docs/current/sql-createtrigger.html)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -2,12 +2,12 @@ import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
title: 'Query Optimization',
description: 'Choosing indexes',
description: 'Choosing indexes to improve your query performance.',
subtitle: 'Choosing indexes to improve your query performance.',
footerHelpType: 'postgres',
}
When working with PostgreSQL, or any relational database, indexing is key to improving query performance.
Aligning indexes with common query patterns can speed up data retrieval by an order of magnitude.
When working with Postgres, or any relational database, indexing is key to improving query performance. Aligning indexes with common query patterns can speed up data retrieval by an order of magnitude.
This guide is intended to:
@@ -90,7 +90,7 @@ explain select * from customers where sign_up_date > 25;
### Use Appropriate Index Types
PostgreSQL offers various index types like [B-tree, Hash, GIN, etc](https://www.postgresql.org/docs/current/indexes-types.html). Select the type that best suits your data and query pattern. Using the right index type can make a significant difference. For example, using a BRIN index on a field that always increases and lives within a table that updates infrequently - like `created_at` on an `orders` table - routinely results in indexes that are +10x smaller than the equivalent default B-tree index. That translates into better scalability.
Postgres offers various index types like [B-tree, Hash, GIN, etc](https://www.Postgres.org/docs/current/indexes-types.html). Select the type that best suits your data and query pattern. Using the right index type can make a significant difference. For example, using a BRIN index on a field that always increases and lives within a table that updates infrequently - like `created_at` on an `orders` table - routinely results in indexes that are +10x smaller than the equivalent default B-tree index. That translates into better scalability.
```sql
create index idx_orders_created_at ON customers using brin(created_at);
@@ -127,7 +127,7 @@ analyze customers;
---
By following this guide, you'll be able to discern where indexes can optimize queries and enhance your PostgreSQL performance. Remember that each database is unique, so always consider the specific context and use case of your queries.
By following this guide, you'll be able to discern where indexes can optimize queries and enhance your Postgres performance. Remember that each database is unique, so always consider the specific context and use case of your queries.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -1,34 +0,0 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'timeouts',
title: 'Timeouts',
description: 'Timeouts and optimization',
}
By default, Supabase limits the maximum statement execution time to _8 seconds_ for users accessing the API. Additionally, all users are subject to a global limit of _2 minutes_. This serves as a backstop against resource exhaustion due to either poorly written queries, or abusive usage.
### Changing the default timeout
The timeout values were picked as a reasonable default for the majority of use-cases, but can be modified using the [`alter role`](https://www.postgresql.org/docs/current/sql-alterrole.html) statement:
```sql
-- For API users
alter role authenticator set statement_timeout = '15s';
```
You can also update the statement timeout for a session:
```sql
set statement_timeout to 60000; -- 1 minute in milliseconds
```
### Statement Optimization
All Supabase projects come with the [`pg_stat_statements`](https://www.postgresql.org/docs/current/pgstatstatements.html) extension installed, which tracks planning and execution statistics for all statements executed against it. These statistics can be used in order to diagnose the performance of your project.
This data can further be used in conjunction with the [`explain`](https://www.postgresql.org/docs/current/using-explain.html) functionality of Postgres to optimize your usage.
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
+4 -7
View File
@@ -4,24 +4,21 @@ export const meta = {
id: 'webhooks',
title: 'Database Webhooks',
description: 'Trigger external payloads on database events.',
video: 'https://www.youtube.com/v/codAs9-NeHM',
subtitle: 'Trigger external payloads on database events.',
tocVideo: 'codAs9-NeHM',
}
Database Webhooks allow you to send real-time data from your database to another system whenever a table event occurs.
You can hook into three table events: `INSERT`, `UPDATE`, and `DELETE`. All events are fired _after_ a database row is changed.
## Webhooks vs Triggers
Database Webhooks are very similar to triggers, and that's because Database Webhooks are just a convenience wrapper around triggers
using the [pg_net](/docs/guides/database/extensions/pgnet) extension. This extension is asynchronous, and therefore will not block your database changes for long-running network requests.
This video demonstrates how you can create a new customer in Stripe each time a row is inserted into a `profiles` table:
<Admonition type="note">
Database Webhooks were previously known as Function Hooks.
</Admonition>
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/codAs9-NeHM"
+1 -40
View File
@@ -138,46 +138,7 @@ For working with JWT and Auth functions.
### Roles
Supabase creates several default roles in your Postgres database. To restore defaults at any time you can run the commands inside the
[schema initialization scripts](https://github.com/supabase/postgres/tree/develop/migrations/db/init-scripts). Remember to change your
[role passwords](/docs/guides/self-hosting/docker#securing-your-setup) before deploying to production environments.
##### `postgres`
The default PostgreSQL role. This has admin privileges.
##### `anon`
For "anonymous access". This is the role which the API (PostgREST) will use when a user _is not_ logged in.
##### `authenticator`
A special role for the API (PostgREST). It has very limited access, and is used to validate a JWT and then
"change into" another role determined by the JWT verification.
##### `authenticated`
For "authenticated access". This is the role which the API (PostgREST) will use when a user _is_ logged in.
##### `service_role`
For elevated access. This role is used by the API (PostgREST) to bypass Row Level Security.
##### `supabase_auth_admin`
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `auth` schema.
##### `supabase_storage_admin`
Used by the Auth middleware to connect to the database and run migration. Access is scoped to the `storage` schema.
##### `dashboard_user`
For running commands via the Supabase UI.
##### `supabase_admin`
Supabase Administrative role for maintaining your database.
Supabase creates several [default roles](/docs/guides/database/postgres-roles) in your Postgres database. To restore defaults at any time you can run the commands inside the [schema initialization scripts](https://github.com/supabase/postgres/tree/develop/migrations/db/init-scripts). Remember to change your [role passwords](/docs/guides/self-hosting/docker#securing-your-setup) before deploying to production environments.
### Realtime Logs
+15
View File
@@ -2273,4 +2273,19 @@ module.exports = [
destination: '/docs/guides/platform/oauth-apps/build-a-supabase-integration',
},
{ permanent: true, source: '/docs/reference/cli/config', destination: '/docs/guides/cli/config' },
{
permanent: true,
source: '/docs/guides/database/timeouts',
destination: '/docs/guides/database/postgres/configuration',
},
{
permanent: true,
source: '/docs/guides/database/managing-timezones',
destination: '/docs/guides/database/postgres/configuration',
},
{
permanent: true,
source: '/docs/guides/database/managing-passwords',
destination: '/docs/guides/database/postgres/roles#passwords',
},
]