From ce808954dfd6407a8a5a23cd372871b342f41a26 Mon Sep 17 00:00:00 2001 From: Chase Granberry Date: Tue, 2 Jul 2024 11:46:29 -0700 Subject: [PATCH] docs: init Realtime authz docs (#22803) * chore: init Realtime authz docs * fix: some Vale fixes * fix: run Prettier * fix: Prettier * fix: add note to contact us with placeholder link * fix: swap rules for policies * fix: lots of changes * fix: update for latest Authz model * fix: nits * fix: more nits * fix: nit * edits * fix: remove realtime.channel names and fix `in` filters * fix: caps * fix: moar * fix: change h3 to h4 * fix: clarify interaction with postgres_changes * fix: added simple steps to enable * Update apps/docs/content/guides/realtime/authorization.mdx Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> * Update apps/docs/content/guides/realtime/authorization.mdx Co-authored-by: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> * Update apps/docs/content/guides/realtime/authorization.mdx Co-authored-by: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> * Update apps/docs/content/guides/realtime/authorization.mdx Co-authored-by: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> * fix: add clarity to how it works * fix: `features` to `extensions` * fix: clarity * fix: spelling * fix: add detail about the test query. * updates * updates * updates * fix: update schema * fix: prettier * fix: rewrite section * fix: create tables sql * fix: shorten policy names --------- Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> Co-authored-by: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> Co-authored-by: Wen Bo Xie --- .../NavigationMenu.constants.ts | 5 + .../content/guides/realtime/authorization.mdx | 290 ++++++++++++++++++ .../docs/content/guides/realtime/concepts.mdx | 20 +- .../nextjs-authorization-demo/README.md | 12 +- 4 files changed, 309 insertions(+), 18 deletions(-) create mode 100644 apps/docs/content/guides/realtime/authorization.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index fc1d9593c16..e6bf73bdf40 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -1296,6 +1296,11 @@ export const realtime: NavMenuConstant = { }, ], }, + { + name: 'Security', + url: undefined, + items: [{ name: 'Authorization', url: '/guides/realtime/authorization' }], + }, { name: 'Guides', url: undefined, diff --git a/apps/docs/content/guides/realtime/authorization.mdx b/apps/docs/content/guides/realtime/authorization.mdx new file mode 100644 index 00000000000..17f4d6862e1 --- /dev/null +++ b/apps/docs/content/guides/realtime/authorization.mdx @@ -0,0 +1,290 @@ +--- +id: 'authorization' +title: 'Realtime Authorization' +description: 'Authorization for Supabase Realtime' +sidebar_label: 'Authorization' +--- + +You can control client access to Realtime [Broadcast](/docs/guides/realtime/broadcast) and [Presence](/docs/guides/realtime/presence) by adding Row Level Security policies to the `realtime.messages` table. Each RLS policy can map to a specific action a client can take: + +- Control which clients can broadcast to a Channel +- Control which clients can receive broadcasts from a Channel +- Control which clients can publish their presence to a Channel +- Control which clients can receive messages about the presence of other clients + + + +Realtime Authorization is in Public Alpha. To use Authorization for your Realtime Channels, use `supabase-js` version `v2.44.0` or later. + + + +## How it works + +Realtime uses the `messages` table in your database's `realtime` schema to generate access policies for your clients when they connect to a Channel topic. + +By creating RLS polices on the `realtime.messages` table you can control the access users have to a Channel topic, and features within a Channel topic. + +The validation is done when the user connects. When their WebSocket connection is established and a Channel topic is joined, their permissions are calculated based on: + +- The RLS policies on the `realtime.messages` table +- The user information sent as part of their [Auth JWT](/docs/guides/auth/jwts) +- The request headers +- The Channel topic the user is trying to connect to + +When Realtime generates a policy for a client it performs a query on the `realtime.messages` table and then rolls it back. Realtime does not store any messages in your `realtime.messages` table. + +Using Realtime Authorization involves two steps: + +- In your database, create RLS policies on the `realtime.messages` +- In your client, instantiate the Realtime Channel with the `config` option `private: true` + + + +Increased RLS complexity can impact database performance and connection time, leading to higher connection latency and decreased join rates. + + + +## Helper functions + +You can use the following helper functions when writing RLS policies: + +### `realtime.topic` + +Returns the Channel topic the user is attempting to connect to. + +```sql +create policy "authenticated can read all messages on topic" +on "realtime"."messages" +for select +to authenticated +using ( + (select realtime.topic()) = 'room-1' +); +``` + +## Examples + +The following examples use this schema: + +```sql +create table public.rooms ( + id bigint generated by default as identity primary key, + topic text not null unique +); + +alter table public.rooms enable row level security; + +create table public.profiles ( + id uuid not null references auth.users on delete cascade, + email text NOT NULL, + + primary key (id) +); + +alter table public.profiles enable row level security; + +create table public.rooms_users ( + user_id uuid references auth.users (id), + room_topic text references public.rooms (topic), + created_at timestamptz default current_timestamp +); + +alter table public.rooms_users enable row level security; +``` + +### Broadcast + +The `extension` field on the `realtime.messages` table records the message type. For Broadcast messages, the value of `realtime.messages.extension` is `broadcast`. You can check for this in your RLS policies. + +#### Allow a user to join (and read) a Broadcast topic + +To join a Broadcast Channel, a user must have at least one read or write permission on the Channel topic. + +Here, we allow reads (`select`s) for users who are linked to the requested topic within the relationship table `public.room_users`: + +```sql +create policy "authenticated can receive broadcast" +on "realtime"."messages" +for select +to authenticated +using ( +exists ( + select + ru.user_id + from + rooms_users ru + where + ru.user_id = (select auth.uid()) + and ru.topic = (select realtime.topic()) + and realtime.messages.extension in ('broadcast') + ) +); +``` + +Then, to join a topic with RLS enabled, instantiate the Channel with the `private` option set to `true`. + +```typescript +import { createClient } from 'npm:@supabase/supabase-js@2.38.5' +const url = 'https://.supabase.com' +const apikey = '' + +const client = createClient(url, apikey) + +const channel = client.channel('room-1', { + config: { private: true }, +}) + +channel + .on('broadcast', { event: 'test' }, (payload) => console.log(payload)) + .subscribe((status: string, err: any) => { + if (status === 'SUBSCRIBED') { + console.log('Connected!') + } else { + console.error(err) + } + }) +``` + +#### Allow a user to send a Broadcast message + +To authorize sending Broadcast messages, create a policy for `insert` where the value of `realtime.messages.extension` is `broadcast`. + +Here, we allow writes (sends) for users who are linked to the requested topic within the relationship table `public.room_users`: + +```sql +create policy "authenticated can send broadcast on topic" +on "realtime"."messages" +for insert +to authenticated +with check ( + exists ( + select + ru.user_id + from + rooms_users ru + where + ru.user_id = (select auth.uid()) + and ru.topic = (select realtime.topic()) + and realtime.messages.extension in ('broadcast') + ) +); +``` + +### Presence + +The `extension` field on the `realtime.messages` table records the message type. For Presence messages, the value of `realtime.messages.extension` is `presence`. You can check for this in your RLS policies. + +#### Allow users to listen to Presence messages on a Channel + +Create a policy for `select` on `realtime.messages` where `realtime.messages.extension` is `presence`. + +```sql +create policy "authenticated can listen to presence in topic" +on "realtime"."messages" +for select +to authenticated +using ( + exists ( + select + ru.user_id + from + rooms_users ru + where + ru.user_id = (select auth.uid()) + and ru.topic = (select realtime.topic()) + and realtime.messages.extension in ('presence') + ) +); +``` + +#### Allow users to send Presence messages on a channel + +To update the Presence status for a user create a policy for `insert` on `realtime.messages` where the value of `realtime.messages.extension` is `presence`. + +```sql +create policy "authenticated can track presence on topic" +on "realtime"."messages" +for insert +to authenticated +with check ( + exists ( + select + ru.user_id + from + rooms_users ru + where + ru.user_id = (select auth.uid()) + and ru.name = (select realtime.topic()) + and realtime.messages.extension in ('presence') + ) +); +``` + +### Presence and Broadcast + +Authorize both Presence and Broadcast by including both extensions in the `where` filter. + +#### Broadcast and Presence read + +Authorize Presence and Broadcast read in one RLS policy. + +```sql +create policy "authenticated can listen to broadcast and presence on topic" +on "realtime"."messages" +for select +to authenticated +using ( + exists ( + select + ru.user_id + from + rooms_users ru + where + ru.user_id = (select auth.uid()) + and ru.topic = (select realtime.topic()) + and realtime.messages.extension in ('broadcast', 'presence') + ) +); +``` + +#### Broadcast and Presence write + +Authorize Presence and Broadcast write in one RLS policy. + +```sql +create policy "authenticated can send broadcast and presence on topic" +on "realtime"."messages" +for insert +to authenticated +with check ( + exists ( + select + ru.user_id + from + rooms_users ru + where + ru.user_id = (select auth.uid()) + and ru.name = (select realtime.topic()) + and realtime.messages.extension in ('broadcast', 'presence') + ) +); +``` + +## Interaction with Postgres Changes + +Realtime Postgres Changes are separate from Channel authorization. The `private` Channel option does not apply to Postgres Changes. + +When using Postgres Changes with RLS, database records are sent only to clients who are allowed to read them based on your RLS policies. + +## Updating RLS policies + +Client access polices are cached for the duration of the connection. Your database is not queried for every Channel message. + +Realtime updates the access policy cache for a client based on your RLS polices when: + +- A client connects to Realtime and subscribes to a Channel +- A new JWT is sent to Realtime from a client via the [`access_token` message](docs/guides/realtime/protocol#access-token) + +If a new JWT is never received on the Channel, the client will be disconnected when the JWT expires. + +Make sure to keep the JWT expiration window short. diff --git a/apps/docs/content/guides/realtime/concepts.mdx b/apps/docs/content/guides/realtime/concepts.mdx index ca5a5672497..9b17e13ee76 100644 --- a/apps/docs/content/guides/realtime/concepts.mdx +++ b/apps/docs/content/guides/realtime/concepts.mdx @@ -1,12 +1,12 @@ --- id: 'channels' title: 'Realtime Concepts' -description: 'Learn about channels and other features in Supabase Realtime' -subtitle: 'Learn about channels and other features in Supabase Realtime' +description: 'Learn about Channels and other extensions in Supabase Realtime' +subtitle: 'Learn about Channels and other extensions in Supabase Realtime' sidebar_label: 'Concepts' --- -You can use Supabase Realtime to build real-time applications with collaborative/multiplayer functionality. It includes 3 core features: +You can use Supabase Realtime to build real-time applications with collaborative/multiplayer functionality. It includes 3 core extensions: - [Broadcast](/docs/guides/realtime/broadcast): sends rapid, ephemeral messages to other connected clients. You can use it to track mouse movements, for example. - [Presence](/docs/guides/realtime/presence): sends user state between connected clients. You can use it to show an "online" status, which disappears when a user is disconnected. @@ -57,15 +57,7 @@ Each client maintains their own state, and this is then combined into a "shared When a new client subscribes to a channel, it will immediately receive the channel's latest state in a single message because the state is held by the Realtime server. -## Choosing between broadcast and presence - -We recommend using Broadcast by default, and then Presence when required. Presence utilizes an in-memory conflict-free replicated data type (CRDT) to track and synchronize shared state in an eventually consistent manner. It computes the difference between existing state and new state changes and sends the necessary updates to clients via Broadcast. This is computationally heavy, so you should use it sparingly. If you use Presence, it's best to throttle your changes so that you are sending updates less frequently. - -## Realtime extensions - -Channels provide a generic networking solution. Supabase Realtime is designed to leverage this networking primitive with "extensions". We currently support one extension: Postgres changes. - -### Postgres changes +## Postgres Changes The Postgres Changes extension listens for database changes and sends them to clients. Clients are required to subscribe with a JWT dictating which changes they are allowed to receive based on the database's [Row Level Security](/docs/guides/database/postgres/row-level-security). @@ -86,3 +78,7 @@ const allChanges = client Anyone with access to a valid JWT signed with the project's JWT secret is able to listen to your database's changes, unless tables have [Row Level Security](/docs/guides/database/postgres/row-level-security) enabled and policies in place. Clients can choose to receive `INSERT`, `UPDATE`, `DELETE`, or `*` (all) changes for all changes in a schema, a table in a schema, or a column's value in a table. Your clients should only listen to tables in the `public` schema and you must first enable the tables you want your clients to listen to. + +## Choosing between Broadcast and Presence + +We recommend using Broadcast by default, and then Presence when required. Presence utilizes an in-memory conflict-free replicated data type (CRDT) to track and synchronize shared state in an eventually consistent manner. It computes the difference between existing state and new state changes and sends the necessary updates to clients via Broadcast. This is computationally heavy, so you should use it sparingly. If you use Presence, it's best to throttle your changes so that you are sending updates less frequently. diff --git a/examples/realtime/nextjs-authorization-demo/README.md b/examples/realtime/nextjs-authorization-demo/README.md index 735b8591549..932fa955ea7 100644 --- a/examples/realtime/nextjs-authorization-demo/README.md +++ b/examples/realtime/nextjs-authorization-demo/README.md @@ -68,19 +68,19 @@ We have to set up RLS policies for the `public` schema tables we created in the > ⚠️ All the RLS policies here are meant for this demo. You may refer to them but make sure that your policies are tailored to your use case and secure your application. ```sql -CREATE POLICY "authenticated users can view all profiles" +CREATE POLICY "authenticated can view all profiles" ON "public"."profiles" AS PERMISSIVE FOR SELECT TO authenticated USING (true); -CREATE POLICY "supabase_auth_admin user can insert profile" +CREATE POLICY "supabase_auth_admin can insert profile" ON "public"."profiles" AS PERMISSIVE FOR INSERT TO supabase_auth_admin WITH CHECK (true); -CREATE POLICY "authenticated users can read rooms" +CREATE POLICY "authenticated can read rooms" ON "public"."rooms" AS PERMISSIVE FOR SELECT TO authenticated @@ -92,7 +92,7 @@ AS PERMISSIVE FOR INSERT TO authenticated WITH CHECK (TRUE); -CREATE POLICY "authenticated users can read rooms_users" +CREATE POLICY "authenticated can read rooms_users" ON "public"."rooms_users" AS PERMISSIVE FOR SELECT TO authenticated @@ -104,7 +104,7 @@ AS PERMISSIVE FOR INSERT TO authenticated WITH CHECK (TRUE); -CREATE POLICY "authenticated user can read broadcast messages and presence state" +CREATE POLICY "authenticated can read broadcast and presence state" ON "realtime"."messages" AS PERMISSIVE FOR SELECT TO authenticated @@ -118,7 +118,7 @@ USING ( ) ); -CREATE POLICY "authenticated user can send broadcast messages and track presence" +CREATE POLICY "authenticated can send broadcast and track presence" ON "realtime"."messages" AS PERMISSIVE FOR INSERT TO authenticated