Skip to main content

This is a drop-in example of Postgres Row Security Level designed for a typical chat application.

Helper functions

Security rules make use of some helper functions:

    DROP function if exists chats.is_auth;
CREATE OR REPLACE FUNCTION chats.is_auth()
RETURNS boolean
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF SECURITY INVOKER
SET search_path = ''
AS $BODY$
BEGIN
return auth.uid() IS NOT NULL;
end;
$BODY$;

DROP function if exists chats.is_owner;
CREATE OR REPLACE FUNCTION chats.is_owner(user_id uuid)
RETURNS boolean
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF SECURITY INVOKER
SET search_path = ''
AS $BODY$
BEGIN
return auth.uid() = user_id;
end;
$BODY$;

DROP function if exists chats.is_member;
CREATE OR REPLACE FUNCTION chats.is_member(members uuid[])
RETURNS boolean
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF SECURITY INVOKER
SET search_path = ''
AS $BODY$
BEGIN
return auth.uid() = ANY(members);
end;
$BODY$;

DROP function if exists chats.is_chat_member;
CREATE OR REPLACE FUNCTION chats.is_chat_member(room_id bigint)
RETURNS boolean
LANGUAGE 'plpgsql'
COST 100
VOLATILE NOT LEAKPROOF SECURITY INVOKER
SET search_path = ''
AS $BODY$
DECLARE
members uuid[];
BEGIN
SELECT "userIds" INTO members
FROM chats.rooms
WHERE id = room_id;
return chats.is_member(members);
end;
$BODY$;

Security rules

Tables

Summary

Table chats.users

  • INSERT : Nobody, this table is populate by trigger on auth.users.
  • SELECT : All users authenticated.
  • UPDATE : Only the user himself.
  • DELETE : Nobody.

Table chats.rooms

  • INSERT : All users authenticated.
  • SELECT : All users who are members of the chat room.
  • UPDATE : All users who are members of the chat room.
  • DELETE : All users who are members of the chat room.

Table chats.messages

  • INSERT : All users who are members of the chat room.
  • SELECT : All users who are members of the chat room.
  • UPDATE : All users who are members of the chat room.
  • DELETE : All users who are members of the chat room.

Security rules implemented:

Table users

    CREATE POLICY "chats.users_grant_create"
ON chats.users
AS PERMISSIVE
FOR INSERT
TO public
WITH CHECK (false); -- Created by trigger

CREATE POLICY "chats.users_grant_read"
ON chats.users
AS PERMISSIVE
FOR SELECT
TO public
USING (chats.is_auth());

CREATE POLICY "chats.users_grant_update"
ON chats.users
AS PERMISSIVE
FOR UPDATE
TO public
USING (chats.is_auth())
WITH CHECK (chats.is_owner(id));

CREATE POLICY "chats.users_grant_delete"
ON chats.users
AS PERMISSIVE
FOR DELETE
TO public
USING (false); -- Delete by foreign key

Table rooms

    CREATE POLICY "chats.rooms_grant_create"
ON chats.rooms
AS PERMISSIVE
FOR INSERT
TO public
WITH CHECK (chats.is_auth());

CREATE POLICY "chats.rooms_grant_read"
ON chats.rooms
AS PERMISSIVE
FOR SELECT
TO public
USING (chats.is_member("userIds"));

CREATE POLICY "chats.rooms_grant_update"
ON chats.rooms
AS PERMISSIVE
FOR UPDATE
TO public
USING (chats.is_member("userIds"))
WITH CHECK (chats.is_member("userIds"));

CREATE POLICY "chats.rooms_grant_delete"
ON chats.rooms
AS PERMISSIVE
FOR DELETE
TO public
USING (chats.is_member("userIds"));

Table messages

    CREATE POLICY "chats.messages_grant_create"
ON chats.messages
AS PERMISSIVE
FOR INSERT
TO public
WITH CHECK (chats.is_chat_member("roomId"));

CREATE POLICY "chats.messages_grant_read"
ON chats.messages
AS PERMISSIVE
FOR SELECT
TO public
USING (chats.is_chat_member("roomId"));

CREATE POLICY "chats.messages_grant_update"
ON chats.messages
AS PERMISSIVE
FOR UPDATE
TO public
USING (chats.is_chat_member("roomId"))
WITH CHECK (chats.is_chat_member("roomId"));

CREATE POLICY "chats.messages_grant_delete"
ON chats.messages
AS PERMISSIVE
FOR DELETE
TO public
USING (chats.is_chat_member("roomId"));

Storage buckets

Summary

Bucket chats_assets

  • INSERT : All users who are members of the chat room.
  • SELECT : All users who are members of the chat room.
  • UPDATE : All users who are members of the chat room.
  • DELETE : All users who are members of the chat room.

Bucket chats_user_avatar

  • INSERT : Only the user himself.
  • SELECT : All users authenticated.
  • UPDATE : Only the user himself.
  • DELETE : Only the user himself.

Security rules implemented:

Bucket chats_assets

    DROP policy IF EXISTS "storage.object_grant_create_auth_chats_assets"
ON storage.objects;
create policy "storage.object_grant_create_auth_chats_assets"
on storage.objects for insert
with check (
bucket_id = 'chats_assets'
and
chats.is_chat_member((storage.foldername(name))[1]::bigint));

DROP policy IF EXISTS "storage.object_grant_read_auth_chats_assets"
ON storage.objects;
create policy "storage.object_grant_read_auth_chats_assets"
on storage.objects for select
using (
bucket_id = 'chats_assets'
and
chats.is_chat_member((storage.foldername(name))[1]::bigint));

DROP policy IF EXISTS "storage.object_grant_update_auth_chats_assets"
ON storage.objects;
create policy "storage.object_grant_update_auth_chats_assets"
on storage.objects for update
using (
bucket_id = 'chats_assets'
and
chats.is_chat_member((storage.foldername(name))[1]::bigint))
with check (
bucket_id = 'chats_assets'
and
chats.is_chat_member((storage.foldername(name))[1]::bigint));

DROP policy IF EXISTS "storage.object_grant_delete_auth_chats_assets"
ON storage.objects;
create policy "storage.object_grant_delete_auth_chats_assets"
on storage.objects for delete
using (
bucket_id = 'chats_assets'
and
chats.is_chat_member((storage.foldername(name))[1]::bigint));

Bucket chats_user_avatar

    DROP policy IF EXISTS "storage.object_grant_create_auth_chats_user_avatar"
ON storage.objects;
create policy "storage.object_grant_create_auth_chats_user_avatar"
on storage.objects for insert
with check (
bucket_id = 'chats_user_avatar'
and
chats.is_owner((storage.foldername(name))[1]::uuid));

DROP policy IF EXISTS "storage.object_grant_read_auth_chats_user_avatar"
ON storage.objects;
create policy "storage.object_grant_read_auth_chats_user_avatar"
on storage.objects for select
using (
bucket_id = 'chats_user_avatar'
and
chats.is_auth());

DROP policy IF EXISTS "storage.object_grant_update_auth_chats_user_avatar"
ON storage.objects;
create policy "storage.object_grant_update_auth_chats_user_avatar"
on storage.objects for update
using (
bucket_id = 'chats_user_avatar'
and
chats.is_owner((storage.foldername(name))[1]::uuid))
with check (
bucket_id = 'chats_user_avatar'
and
chats.is_owner((storage.foldername(name))[1]::uuid));

DROP policy IF EXISTS "storage.object_grant_delete_auth_chats_user_avatar"
ON storage.objects;
create policy "storage.object_grant_delete_auth_chats_user_avatar"
on storage.objects for delete
using (
bucket_id = 'chats_user_avatar'
and
chats.is_owner((storage.foldername(name))[1]::uuid));

To learn more head over to the Postgres Row Security Level website.