From 26511b8886e05fae18acfa9647a53309e3ee3085 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Mon, 29 Apr 2024 19:43:23 +0700 Subject: [PATCH] Update database schema and add notification test This commit updates the database schema to enhance security by revoking access of all tables from authenticated and service_role before granting select or relevant access. It further modifies SQL function syntax formatting and defines new types for notifications. Also, it adds a new test for the notification functionality to check the permission levels of different users and their ability to insert and read notifications. --- .../migrations/20221215192558_schema.sql | 180 ++++++++++++------ .../tests/database/notifications.test.sql | 77 ++++++++ 2 files changed, 201 insertions(+), 56 deletions(-) create mode 100644 apps/web/supabase/tests/database/notifications.test.sql diff --git a/apps/web/supabase/migrations/20221215192558_schema.sql b/apps/web/supabase/migrations/20221215192558_schema.sql index 9add0104d..5f5e24607 100644 --- a/apps/web/supabase/migrations/20221215192558_schema.sql +++ b/apps/web/supabase/migrations/20221215192558_schema.sql @@ -168,6 +168,7 @@ comment on column public.config.enable_team_account_billing is 'Enable billing f comment on column public.config.billing_provider is 'The billing provider to use'; +-- RLS(config) alter table public.config enable row level security; -- create config row @@ -180,6 +181,12 @@ insert into values (true, true, true); +-- Revoke all on accounts table from authenticated and service_role +revoke all on public.config +from + authenticated, + service_role; + -- Open up access to config table for authenticated users and service_role grant select @@ -321,6 +328,12 @@ comment on column public.accounts.email is 'The email of the account. For teams, -- Enable RLS on the accounts table alter table "public"."accounts" enable row level security; +-- Revoke all on accounts table from authenticated and service_role +revoke all on public.accounts +from + authenticated, + service_role; + -- Open up access to accounts grant select @@ -456,7 +469,9 @@ begin return NEW; end -$$ language plpgsql set search_path = ''; +$$ language plpgsql +set + search_path = ''; -- trigger to protect account fields create trigger protect_account_fields before @@ -556,8 +571,15 @@ create table if not exists primary key (name) ); +-- Revoke all on roles table from authenticated and service_role +revoke all on public.roles +from + authenticated, + service_role; + +-- Open up access to roles table for authenticated users and service_role grant -select +select, insert, delete, update on table public.roles to authenticated, service_role; @@ -646,17 +668,25 @@ comment on column public.accounts_memberships.account_id is 'The account the mem comment on column public.accounts_memberships.account_role is 'The role for the membership'; +-- Revoke all on accounts_memberships table from authenticated and service_role +revoke all on public.accounts_memberships +from + authenticated, + service_role; + -- Open up access to accounts_memberships table for authenticated users and service_role grant select , insert, update, -delete on table public.accounts_memberships to service_role; +delete on table public.accounts_memberships to authenticated, service_role; -- Indexes on the accounts_memberships table create index ix_accounts_memberships_account_id on public.accounts_memberships (account_id); + create index ix_accounts_memberships_user_id on public.accounts_memberships (user_id); + create index ix_accounts_memberships_account_role on public.accounts_memberships (account_role); -- Enable RLS on the accounts_memberships table @@ -930,6 +960,12 @@ comment on column public.role_permissions.permission is 'The permission for the -- Indexes on the role_permissions table create index ix_role_permissions_role on public.role_permissions (role); +-- Revoke all on role_permissions table from authenticated and service_role +revoke all on public.role_permissions +from + authenticated, + service_role; + -- Open up access to role_permissions table for authenticated users and service_role grant select @@ -1174,10 +1210,14 @@ comment on column public.invitations.email is 'The email of the user being invit -- Indexes on the invitations table create index ix_invitations_account_id on public.invitations (account_id); -create index ix_invitations_role on public.invitations (role); --- Open up access to invitations table for authenticated users and --- service_role +-- Revoke all on invitations table from authenticated and service_role +revoke all on public.invitations +from + authenticated, + service_role; + +-- Open up access to invitations table for authenticated users and service_role grant select , @@ -1381,8 +1421,13 @@ comment on column public.billing_customers.email is 'The email of the billing cu -- Indexes on the billing_customers table create index ix_billing_customers_account_id on public.billing_customers (account_id); --- Open up access to billing_customers table for authenticated users --- and service_role +-- Revoke all on billing_customers table from authenticated and service_role +revoke all on public.billing_customers +from + authenticated, + service_role; + +-- Open up relevant access to billing_customers table for authenticated users and service_role grant select , @@ -1390,15 +1435,15 @@ select update, delete on table public.billing_customers to service_role; --- Enable RLS on billing_customers table -alter table public.billing_customers enable row level security; - -- Open up access to billing_customers table for authenticated users grant select on table public.billing_customers to authenticated, service_role; +-- Enable RLS on billing_customers table +alter table public.billing_customers enable row level security; + -- RLS on the billing_customers table -- SELECT(billing_customers): -- Users can read account subscriptions on an account they are a member of @@ -1461,7 +1506,13 @@ comment on column public.subscriptions.active is 'Whether the subscription is ac comment on column public.subscriptions.billing_customer_id is 'The billing customer ID for the subscription'; --- Open up access to subscriptions table for authenticated users and service_role +-- Revoke all on subscriptions table from authenticated and service_role +revoke all on public.subscriptions +from + authenticated, + service_role; + +-- Open up relevant access to subscriptions table for authenticated users and service_role grant select , @@ -1685,8 +1736,13 @@ comment on column public.subscription_items.created_at is 'The creation date of comment on column public.subscription_items.updated_at is 'The last update date of the item'; --- Open up access to subscription_items table for authenticated users --- and service_role +-- Revoke all access to subscription_items table for authenticated users and service_role +revoke all on public.subscription_items +from + authenticated, + service_role; + +-- Open up relevant access to subscription_items table for authenticated users and service_role grant select on table public.subscription_items to authenticated, @@ -1759,6 +1815,12 @@ comment on column public.orders.status is 'The status of the order'; comment on column public.orders.billing_customer_id is 'The billing customer ID for the order'; +-- Revoke all access to orders table for authenticated users and service_role +revoke all on public.orders +from + authenticated, + service_role; + -- Open up access to orders table for authenticated users and service_role grant select @@ -1833,7 +1895,13 @@ comment on column public.order_items.created_at is 'The creation date of the ite comment on column public.order_items.updated_at is 'The last update date of the item'; --- Open up access to order_items table for authenticated users and service_role +-- Revoke all access to order_items table for authenticated users and service_role +revoke all on public.order_items +from + authenticated, + service_role; + +-- Open up relevant access to order_items table for authenticated users and service_role grant select on table public.order_items to authenticated, @@ -1975,23 +2043,22 @@ execute on function public.upsert_order ( * We create the schema for the notifications. Notifications are the notifications for an account. * ------------------------------------------------------- */ -create type public.notification_channel as enum ('in_app', 'email'); -create type public.notification_type as enum ('info', 'warning', 'error'); +create type public.notification_channel as enum('in_app', 'email'); -create table if not exists public.notifications ( - id bigint generated always as identity primary key, - account_id uuid not null references public.accounts(id) on delete cascade, - type public.notification_type not null default 'info', - body varchar(5000) not null, - link varchar(255), - entity_id text, - entity_type text, - channel public.notification_channel not null default 'in_app', - language_code varchar(10) not null default 'en', - dismissed boolean not null default false, - expires_at timestamptz default (now() + interval '1 month'), - created_at timestamptz not null default now() -); +create type public.notification_type as enum('info', 'warning', 'error'); + +create table if not exists + public.notifications ( + id bigint generated always as identity primary key, + account_id uuid not null references public.accounts (id) on delete cascade, + type public.notification_type not null default 'info', + body varchar(5000) not null, + link varchar(255), + channel public.notification_channel not null default 'in_app', + dismissed boolean not null default false, + expires_at timestamptz default (now() + interval '1 month'), + created_at timestamptz not null default now() + ); comment on table notifications is 'The notifications for an account'; @@ -2003,37 +2070,37 @@ comment on column notifications.body is 'The body of the notification'; comment on column notifications.link is 'The link for the notification'; -comment on column notifications.entity_id is 'The entity ID for the notification'; - -comment on column notifications.entity_type is 'The entity type for the notification'; - comment on column notifications.channel is 'The channel for the notification'; -comment on column notifications.language_code is 'The language code for the notification'; - comment on column notifications.dismissed is 'Whether the notification has been dismissed'; comment on column notifications.expires_at is 'The expiry date for the notification'; comment on column notifications.created_at is 'The creation date for the notification'; --- Open up access to order_items table for authenticated users and service_role -grant -select, update - on table public.notifications to authenticated, +-- Revoke all access to notifications table for authenticated users and service_role +revoke all on public.notifications +from + authenticated, service_role; +-- Open up relevant access to notifications table for authenticated users and service_role +grant +select +, +update on table public.notifications to authenticated, +service_role; + grant insert on table public.notifications to service_role; -- enable realtime -alter - publication supabase_realtime add table public.notifications; +alter publication supabase_realtime +add table public.notifications; -- Indexes -- Indexes on the notifications table - -- index for selecting notifications for an account that are not dismissed and not expired -create index idx_notifications_account_dismissed on notifications(account_id, dismissed, expires_at); +create index idx_notifications_account_dismissed on notifications (account_id, dismissed, expires_at); -- RLS alter table public.notifications enable row level security; @@ -2052,8 +2119,8 @@ select -- UPDATE(notifications): -- Users can set notifications to read on an account they are a member of -create policy notifications_update_self on public.notifications for -update +create policy notifications_update_self on public.notifications +for update to authenticated using ( account_id = ( select @@ -2064,10 +2131,10 @@ update -- Function "kit.update_notification_dismissed_status" -- Make sure the only updatable field is the dismissed status and nothing else -create or replace function kit.update_notification_dismissed_status() -returns trigger -set search_path to '' -as $$ +create +or replace function kit.update_notification_dismissed_status () returns trigger +set + search_path to '' as $$ begin old.dismissed := new.dismissed; @@ -2080,8 +2147,9 @@ end; $$ language plpgsql; -- add trigger when updating a notification to update the dismissed status -create trigger update_notification_dismissed_status before update on public.notifications for each row -execute procedure kit.update_notification_dismissed_status(); +create trigger update_notification_dismissed_status before +update on public.notifications for each row +execute procedure kit.update_notification_dismissed_status (); /** * ------------------------------------------------------- @@ -2090,7 +2158,6 @@ execute procedure kit.update_notification_dismissed_status(); * We use this for ensure unique slugs for accounts. * ------------------------------------------------------- */ - -- Create a function to slugify a string -- useful for turning an account name into a unique slug create @@ -2134,7 +2201,9 @@ or replace function kit.slugify ("value" text) returns text as $$ "value" from "trimmed"; -$$ language SQL strict immutable set search_path to ''; +$$ language SQL strict immutable +set + search_path to ''; grant execute on function kit.slugify (text) to service_role, @@ -2259,7 +2328,6 @@ execute procedure kit.setup_new_user (); * We create the schema for the functions. Functions are the custom functions for the application. * ------------------------------------------------------- */ - -- Function "public.create_team_account" -- Create a team account if team accounts are enabled create diff --git a/apps/web/supabase/tests/database/notifications.test.sql b/apps/web/supabase/tests/database/notifications.test.sql new file mode 100644 index 000000000..fcd57642b --- /dev/null +++ b/apps/web/supabase/tests/database/notifications.test.sql @@ -0,0 +1,77 @@ +BEGIN; +create extension "basejump-supabase_test_helpers" version '0.0.6'; + +select no_plan(); + +--- we insert a user into auth.users and return the id into user_id to use + +select tests.create_supabase_user('test1', 'test1@test.com'); + +select tests.create_supabase_user('test2'); + +select tests.authenticate_as('test1'); + +-- users cannot insert into notifications +select throws_ok( + $$ insert into public.notifications(account_id, body) values (tests.get_supabase_uid('test1'), 'test'); $$, + 'permission denied for table notifications' +); + +set local role service_role; + +-- service role can insert into notifications +select lives_ok( + $$ insert into public.notifications(account_id, body) values (tests.get_supabase_uid('test1'), 'test'); $$, + 'service role can insert into notifications' +); + +select tests.authenticate_as('test1'); + +-- user can read their own notifications +select row_eq( + $$ select account_id, body from public.notifications where account_id = tests.get_supabase_uid('test1'); $$, + row (tests.get_supabase_uid('test1'), 'test'::varchar), + 'user can read their own notifications' +); + +-- user can read their team notifications +select makerkit.set_identifier('primary_owner', 'test@makerkit.dev'); +select makerkit.set_identifier('owner', 'owner@makerkit.dev'); +select makerkit.set_identifier('member', 'member@makerkit.dev'); +select makerkit.set_identifier('custom', 'custom@makerkit.dev'); + +set local role service_role; + +-- service role can insert into notifications +select lives_ok( + $$ insert into public.notifications(account_id, body) values (makerkit.get_account_id_by_slug('makerkit'), 'test'); $$, + 'service role can insert into notifications' +); + +select tests.authenticate_as('member'); + +select row_eq( + $$ select account_id, body from public.notifications where account_id = makerkit.get_account_id_by_slug('makerkit'); $$, + row (makerkit.get_account_id_by_slug('makerkit'), 'test'::varchar), + 'user can read their team notifications' +); + +-- foreigners + +select tests.authenticate_as('test2'); + +-- foreigner cannot read other user's notifications +select is_empty( + $$ select account_id, body from public.notifications where account_id = tests.get_supabase_uid('test1'); $$, + 'foreigner cannot read other users notifications' +); + +-- foreigner cannot read other teams notifications +select is_empty( + $$ select account_id, body from public.notifications where account_id = makerkit.get_account_id_by_slug('makerkit'); $$, + 'foreigner cannot read other teams notifications' +); + +select * from finish(); + +rollback; \ No newline at end of file