B2B-88: add starter kit structure and elements
This commit is contained in:
312
supabase copy/schemas/05-memberships.sql
Normal file
312
supabase copy/schemas/05-memberships.sql
Normal file
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Section: Memberships
|
||||
* We create the schema for the memberships. Memberships are the memberships for an account. For example, a user might be a member of an account with the role 'owner'.
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
|
||||
-- Account Memberships table
|
||||
create table if not exists
|
||||
public.accounts_memberships (
|
||||
user_id uuid references auth.users on delete cascade not null,
|
||||
account_id uuid references public.accounts (id) on delete cascade not null,
|
||||
account_role varchar(50) references public.roles (name) not null,
|
||||
created_at timestamptz default current_timestamp not null,
|
||||
updated_at timestamptz default current_timestamp not null,
|
||||
created_by uuid references auth.users,
|
||||
updated_by uuid references auth.users,
|
||||
primary key (user_id, account_id)
|
||||
);
|
||||
|
||||
comment on table public.accounts_memberships is 'The memberships for an account';
|
||||
|
||||
comment on column public.accounts_memberships.account_id is 'The account the membership is for';
|
||||
|
||||
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 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
|
||||
alter table public.accounts_memberships enable row level security;
|
||||
|
||||
-- Function "kit.prevent_account_owner_membership_delete"
|
||||
-- Trigger to prevent a primary owner from being removed from an account
|
||||
create
|
||||
or replace function kit.prevent_account_owner_membership_delete () returns trigger
|
||||
set
|
||||
search_path = '' as $$
|
||||
begin
|
||||
if exists(
|
||||
select
|
||||
1
|
||||
from
|
||||
public.accounts
|
||||
where
|
||||
id = old.account_id
|
||||
and primary_owner_user_id = old.user_id) then
|
||||
raise exception 'The primary account owner cannot be removed from the account membership list';
|
||||
|
||||
end if;
|
||||
|
||||
return old;
|
||||
|
||||
end;
|
||||
|
||||
$$ language plpgsql;
|
||||
|
||||
create
|
||||
or replace trigger prevent_account_owner_membership_delete_check before delete on public.accounts_memberships for each row
|
||||
execute function kit.prevent_account_owner_membership_delete ();
|
||||
|
||||
-- Function "kit.prevent_memberships_update"
|
||||
-- Trigger to prevent updates to account memberships with the exception of the account_role
|
||||
create
|
||||
or replace function kit.prevent_memberships_update () returns trigger
|
||||
set
|
||||
search_path = '' as $$
|
||||
begin
|
||||
if new.account_role <> old.account_role then
|
||||
return new;
|
||||
end if;
|
||||
|
||||
raise exception 'Only the account_role can be updated';
|
||||
|
||||
end; $$ language plpgsql;
|
||||
|
||||
create
|
||||
or replace trigger prevent_memberships_update_check before
|
||||
update on public.accounts_memberships for each row
|
||||
execute function kit.prevent_memberships_update ();
|
||||
|
||||
-- Function "public.has_role_on_account"
|
||||
-- Function to check if a user has a role on an account
|
||||
create
|
||||
or replace function public.has_role_on_account (
|
||||
account_id uuid,
|
||||
account_role varchar(50) default null
|
||||
) returns boolean language sql security definer
|
||||
set
|
||||
search_path = '' as $$
|
||||
select
|
||||
exists(
|
||||
select
|
||||
1
|
||||
from
|
||||
public.accounts_memberships membership
|
||||
where
|
||||
membership.user_id = (select auth.uid())
|
||||
and membership.account_id = has_role_on_account.account_id
|
||||
and((membership.account_role = has_role_on_account.account_role
|
||||
or has_role_on_account.account_role is null)));
|
||||
$$;
|
||||
|
||||
grant
|
||||
execute on function public.has_role_on_account (uuid, varchar) to authenticated;
|
||||
|
||||
-- Function "public.is_team_member"
|
||||
-- Check if a user is a team member of an account or not
|
||||
create
|
||||
or replace function public.is_team_member (account_id uuid, user_id uuid) returns boolean language sql security definer
|
||||
set
|
||||
search_path = '' as $$
|
||||
select
|
||||
exists(
|
||||
select
|
||||
1
|
||||
from
|
||||
public.accounts_memberships membership
|
||||
where
|
||||
public.has_role_on_account(account_id)
|
||||
and membership.user_id = is_team_member.user_id
|
||||
and membership.account_id = is_team_member.account_id);
|
||||
$$;
|
||||
|
||||
grant
|
||||
execute on function public.is_team_member (uuid, uuid) to authenticated,
|
||||
service_role;
|
||||
|
||||
-- RLS
|
||||
-- SELECT(roles)
|
||||
-- authenticated users can query roles
|
||||
create policy roles_read on public.roles for
|
||||
select
|
||||
to authenticated using (
|
||||
true
|
||||
);
|
||||
|
||||
-- Function "public.can_action_account_member"
|
||||
-- Check if a user can perform management actions on an account member
|
||||
create
|
||||
or replace function public.can_action_account_member (target_team_account_id uuid, target_user_id uuid) returns boolean
|
||||
set
|
||||
search_path = '' as $$
|
||||
declare
|
||||
permission_granted boolean;
|
||||
target_user_hierarchy_level int;
|
||||
current_user_hierarchy_level int;
|
||||
is_account_owner boolean;
|
||||
target_user_role varchar(50);
|
||||
begin
|
||||
if target_user_id = auth.uid() then
|
||||
raise exception 'You cannot update your own account membership with this function';
|
||||
end if;
|
||||
|
||||
-- an account owner can action any member of the account
|
||||
if public.is_account_owner(target_team_account_id) then
|
||||
return true;
|
||||
end if;
|
||||
|
||||
-- check the target user is the primary owner of the account
|
||||
select
|
||||
exists (
|
||||
select
|
||||
1
|
||||
from
|
||||
public.accounts
|
||||
where
|
||||
id = target_team_account_id
|
||||
and primary_owner_user_id = target_user_id) into is_account_owner;
|
||||
|
||||
if is_account_owner then
|
||||
raise exception 'The primary account owner cannot be actioned';
|
||||
end if;
|
||||
|
||||
-- validate the auth user has the required permission on the account
|
||||
-- to manage members of the account
|
||||
select
|
||||
public.has_permission(auth.uid(), target_team_account_id,
|
||||
'members.manage'::public.app_permissions) into
|
||||
permission_granted;
|
||||
|
||||
-- if the user does not have the required permission, raise an exception
|
||||
if not permission_granted then
|
||||
raise exception 'You do not have permission to action a member from this account';
|
||||
end if;
|
||||
|
||||
-- get the role of the target user
|
||||
select
|
||||
am.account_role,
|
||||
r.hierarchy_level
|
||||
from
|
||||
public.accounts_memberships as am
|
||||
join
|
||||
public.roles as r on am.account_role = r.name
|
||||
where
|
||||
am.account_id = target_team_account_id
|
||||
and am.user_id = target_user_id
|
||||
into target_user_role, target_user_hierarchy_level;
|
||||
|
||||
-- get the hierarchy level of the current user
|
||||
select
|
||||
r.hierarchy_level into current_user_hierarchy_level
|
||||
from
|
||||
public.roles as r
|
||||
join
|
||||
public.accounts_memberships as am on r.name = am.account_role
|
||||
where
|
||||
am.account_id = target_team_account_id
|
||||
and am.user_id = auth.uid();
|
||||
|
||||
if target_user_role is null then
|
||||
raise exception 'The target user does not have a role on the account';
|
||||
end if;
|
||||
|
||||
if current_user_hierarchy_level is null then
|
||||
raise exception 'The current user does not have a role on the account';
|
||||
end if;
|
||||
|
||||
-- check the current user has a higher role than the target user
|
||||
if current_user_hierarchy_level >= target_user_hierarchy_level then
|
||||
raise exception 'You do not have permission to action a member from this account';
|
||||
end if;
|
||||
|
||||
return true;
|
||||
|
||||
end;
|
||||
|
||||
$$ language plpgsql;
|
||||
|
||||
grant
|
||||
execute on function public.can_action_account_member (uuid, uuid) to authenticated,
|
||||
service_role;
|
||||
|
||||
-- RLS
|
||||
-- SELECT(accounts_memberships):
|
||||
-- Users can read their team members account memberships
|
||||
create policy accounts_memberships_read on public.accounts_memberships for
|
||||
select
|
||||
to authenticated using (
|
||||
(
|
||||
(
|
||||
select
|
||||
auth.uid ()
|
||||
) = user_id
|
||||
)
|
||||
or is_team_member (account_id, user_id)
|
||||
);
|
||||
|
||||
create
|
||||
or replace function public.is_account_team_member (target_account_id uuid) returns boolean
|
||||
set
|
||||
search_path = '' as $$
|
||||
select exists(
|
||||
select 1
|
||||
from public.accounts_memberships as membership
|
||||
where public.is_team_member (membership.account_id, target_account_id)
|
||||
);
|
||||
$$ language sql;
|
||||
|
||||
grant
|
||||
execute on function public.is_account_team_member (uuid) to authenticated,
|
||||
service_role;
|
||||
|
||||
-- RLS on the accounts table
|
||||
-- SELECT(accounts):
|
||||
-- Users can read the an account if
|
||||
-- - they are the primary owner of the account
|
||||
-- - they have a role on the account
|
||||
-- - they are reading an account of the same team
|
||||
create policy accounts_read on public.accounts for
|
||||
select
|
||||
to authenticated using (
|
||||
(
|
||||
(
|
||||
select
|
||||
auth.uid ()
|
||||
) = primary_owner_user_id
|
||||
)
|
||||
or public.has_role_on_account (id)
|
||||
or public.is_account_team_member (id)
|
||||
);
|
||||
|
||||
-- DELETE(accounts_memberships):
|
||||
-- Users with the required role can remove members from an account or remove their own
|
||||
create policy accounts_memberships_delete on public.accounts_memberships for delete to authenticated using (
|
||||
(
|
||||
user_id = (
|
||||
select
|
||||
auth.uid ()
|
||||
)
|
||||
)
|
||||
or public.can_action_account_member (account_id, user_id)
|
||||
);
|
||||
Reference in New Issue
Block a user