Implement custom roles and improve permissions logic
The commit refactors the handling of account roles and enhances permissions checks. The account role has been shifted to use a string type, providing the ability to define custom roles. It also introduces the RolesDataProvider component, which stipulates role-related data for different forms and tables. The modification goes further to consider user role hierarchy in permissions checks, offering a more granular access control.
This commit is contained in:
@@ -86,13 +86,6 @@ grant usage on schema public to service_role;
|
||||
* We create the enums for the schema
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
/*
|
||||
* Roles
|
||||
- We create the roles for the Supabase MakerKit. These roles are used to manage the permissions for the accounts
|
||||
- The roles are 'owner' and 'member'.
|
||||
- You can add more roles as needed.
|
||||
*/
|
||||
create type public.account_role as enum('owner', 'member');
|
||||
|
||||
/*
|
||||
* Permissions
|
||||
@@ -396,6 +389,34 @@ after
|
||||
update of email on auth.users for each row
|
||||
execute procedure kit.handle_update_user_email ();
|
||||
|
||||
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Section: Roles
|
||||
* We create the schema for the roles. Roles are the roles for an account. For example, an account might have the roles 'owner', 'admin', and 'member'.
|
||||
* -------------------------------------------------------
|
||||
*/
|
||||
-- Account Memberships table
|
||||
create table if not exists public.roles (
|
||||
name varchar(50) not null,
|
||||
hierarchy_level int not null,
|
||||
account_id uuid references public.accounts (id) on delete cascade,
|
||||
is_custom boolean not null default false,
|
||||
unique (hierarchy_level, account_id, is_custom),
|
||||
primary key (name)
|
||||
);
|
||||
|
||||
-- Seed the roles table with default roles 'owner' and 'member'
|
||||
insert into public.roles (name, hierarchy_level) values ('owner', 1);
|
||||
insert into public.roles (name, hierarchy_level) values ('member', 2);
|
||||
|
||||
-- RLS
|
||||
-- SELECT: authenticated users can query roles
|
||||
create policy roles_read on public.roles for
|
||||
select
|
||||
to authenticated
|
||||
using (true);
|
||||
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* Section: Memberships
|
||||
@@ -407,7 +428,7 @@ 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 public.account_role 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,
|
||||
@@ -435,7 +456,7 @@ alter table public.accounts_memberships enable row level security;
|
||||
create
|
||||
or replace function public.has_role_on_account (
|
||||
account_id uuid,
|
||||
account_role public.account_role default null
|
||||
account_role varchar(50) default null
|
||||
) returns boolean language sql security definer
|
||||
set
|
||||
search_path = public as $$
|
||||
@@ -453,7 +474,7 @@ set
|
||||
$$;
|
||||
|
||||
grant
|
||||
execute on function public.has_role_on_account (uuid, public.account_role) to authenticated;
|
||||
execute on function public.has_role_on_account (uuid, varchar) to authenticated;
|
||||
|
||||
create
|
||||
or replace function public.is_team_member (account_id uuid, user_id uuid) returns boolean language sql security definer
|
||||
@@ -479,7 +500,8 @@ execute on function public.is_team_member (uuid, uuid) to authenticated;
|
||||
create or replace function kit.can_remove_account_member (target_team_account_id uuid, user_id uuid) returns boolean as $$
|
||||
declare
|
||||
permission_granted boolean;
|
||||
target_user_role public.account_role;
|
||||
target_user_hierarchy_level int;
|
||||
current_user_hierarchy_level int;
|
||||
begin
|
||||
-- validate the auth user has the required permission on the account
|
||||
-- to manage members of the account
|
||||
@@ -494,20 +516,12 @@ begin
|
||||
raise exception 'You cannot remove yourself from the account';
|
||||
end if;
|
||||
|
||||
-- retrieve the user target role in the account
|
||||
select
|
||||
account_role
|
||||
into
|
||||
target_user_role
|
||||
from
|
||||
public.accounts_memberships as membership
|
||||
where
|
||||
membership.account_id = target_team_account_id
|
||||
and membership.user_id = can_remove_account_member.user_id;
|
||||
select hierarchy_level into target_user_hierarchy_level from public.roles where name = target_user_role;
|
||||
select hierarchy_level into current_user_hierarchy_level from public.roles where name = (select account_role from public.accounts_memberships where account_id = target_team_account_id and user_id = auth.uid());
|
||||
|
||||
-- check if the target user is the owner of the account
|
||||
if target_user_role = 'owner' then
|
||||
raise exception 'You cannot remove the primary owner from the account';
|
||||
-- check if the current user has a higher hierarchy level than the target user
|
||||
if current_user_hierarchy_level <= target_user_hierarchy_level then
|
||||
raise exception 'You do not have permission to remove this user from the account';
|
||||
end if;
|
||||
|
||||
return true;
|
||||
@@ -569,7 +583,7 @@ create table
|
||||
public.account_roles (
|
||||
id bigint generated by default as identity primary key,
|
||||
account_id uuid references public.accounts (id) on delete cascade not null,
|
||||
role public.account_role not null,
|
||||
role varchar(50) references public.roles(name) not null,
|
||||
unique (account_id, role)
|
||||
);
|
||||
|
||||
@@ -608,7 +622,7 @@ select
|
||||
create table if not exists
|
||||
public.role_permissions (
|
||||
id bigint generated by default as identity primary key,
|
||||
role public.account_role not null,
|
||||
role varchar(50) references public.roles(name) not null,
|
||||
permission app_permissions not null,
|
||||
unique (role, permission)
|
||||
);
|
||||
@@ -651,6 +665,34 @@ $$ language plpgsql;
|
||||
|
||||
grant execute on function public.has_permission (uuid, uuid, public.app_permissions) to authenticated, service_role;
|
||||
|
||||
create or replace function public.has_more_elevated_role (
|
||||
target_user_id uuid,
|
||||
target_account_id uuid,
|
||||
role_name varchar
|
||||
) returns boolean as $$
|
||||
declare
|
||||
declare is_primary_owner boolean;
|
||||
user_role_hierarchy_level int;
|
||||
target_role_hierarchy_level int;
|
||||
begin
|
||||
select exists (select 1 from public.accounts where id = target_account_id and primary_owner_user_id = target_user_id) into is_primary_owner;
|
||||
|
||||
-- If the user is the primary owner, they have the highest role and can perform any action
|
||||
if is_primary_owner then
|
||||
return true;
|
||||
end if;
|
||||
|
||||
select hierarchy_level into user_role_hierarchy_level from public.roles where name = (select account_role from public.accounts_memberships where account_id = target_account_id and target_user_id = user_id);
|
||||
select hierarchy_level into target_role_hierarchy_level from public.roles where name = role_name;
|
||||
|
||||
-- If the user's role is higher than the target role, they can perform the action
|
||||
return user_role_hierarchy_level < target_role_hierarchy_level;
|
||||
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
grant execute on function public.has_more_elevated_role (uuid, uuid, varchar) to authenticated, service_role;
|
||||
|
||||
-- Enable RLS on the role_permissions table
|
||||
alter table public.role_permissions enable row level security;
|
||||
|
||||
@@ -672,7 +714,7 @@ create table if not exists
|
||||
email varchar(255) not null,
|
||||
account_id uuid references public.accounts (id) on delete cascade not null,
|
||||
invited_by uuid references auth.users on delete cascade not null,
|
||||
role public.account_role not null,
|
||||
role varchar(50) references public.roles (name) not null,
|
||||
invite_token varchar(255) unique not null,
|
||||
created_at timestamptz default current_timestamp not null,
|
||||
updated_at timestamptz default current_timestamp not null,
|
||||
@@ -728,23 +770,24 @@ select
|
||||
to authenticated using (has_role_on_account (account_id));
|
||||
|
||||
-- INSERT: Users can create invitations to users of an account they are a member of
|
||||
-- and have the 'invites.manage' permission
|
||||
-- and have the 'invites.manage' permission AND the target role is not higher than the user's role
|
||||
create policy invitations_create_self on public.invitations for
|
||||
insert
|
||||
to authenticated with check (
|
||||
has_role_on_account (account_id)
|
||||
and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions));
|
||||
public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions) and
|
||||
public.has_more_elevated_role (auth.uid (), account_id, role)
|
||||
);
|
||||
|
||||
-- UPDATE: Users can update invitations to users of an account they are a member of
|
||||
-- and have the 'invites.manage' permission
|
||||
-- and have the 'invites.manage' permission AND the target role is not higher than the user's role
|
||||
create policy invitations_update on public.invitations for
|
||||
update
|
||||
to authenticated using (
|
||||
has_role_on_account (account_id)
|
||||
and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
|
||||
public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
|
||||
and public.has_more_elevated_role (auth.uid (), account_id, role)
|
||||
) with check (
|
||||
has_role_on_account (account_id)
|
||||
and public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
|
||||
public.has_permission (auth.uid (), account_id, 'invites.manage'::app_permissions)
|
||||
and public.has_more_elevated_role (auth.uid (), account_id, role)
|
||||
);
|
||||
|
||||
-- DELETE: Users can delete invitations to users of an account they are a member of
|
||||
@@ -761,7 +804,7 @@ delete
|
||||
create or replace function accept_invitation(token text, user_id uuid) returns void as $$
|
||||
declare
|
||||
target_account_id uuid;
|
||||
target_role public.account_role;
|
||||
target_role varchar(50);
|
||||
begin
|
||||
select
|
||||
account_id,
|
||||
@@ -1192,7 +1235,7 @@ create
|
||||
or replace function public.create_invitation (
|
||||
account_id uuid,
|
||||
email text,
|
||||
role public.account_role
|
||||
role varchar(50)
|
||||
) returns public.invitations as $$
|
||||
declare
|
||||
new_invitation public.invitations;
|
||||
@@ -1283,7 +1326,8 @@ or replace function public.organization_account_workspace (account_slug text) re
|
||||
name varchar(255),
|
||||
picture_url varchar(1000),
|
||||
slug text,
|
||||
role public.account_role,
|
||||
role varchar(50),
|
||||
role_hierarchy_level int,
|
||||
primary_owner_user_id uuid,
|
||||
subscription_status public.subscription_status,
|
||||
permissions public.app_permissions[]
|
||||
@@ -1296,6 +1340,7 @@ begin
|
||||
accounts.picture_url,
|
||||
accounts.slug,
|
||||
accounts_memberships.account_role,
|
||||
roles.hierarchy_level,
|
||||
accounts.primary_owner_user_id,
|
||||
subscriptions.status,
|
||||
array_agg(role_permissions.permission)
|
||||
@@ -1305,13 +1350,15 @@ begin
|
||||
left join public.subscriptions on accounts.id = subscriptions.account_id
|
||||
left join public.role_permissions on accounts_memberships.account_role =
|
||||
role_permissions.role
|
||||
left join public.roles on accounts_memberships.account_role = roles.name
|
||||
where
|
||||
accounts.slug = account_slug
|
||||
and public.accounts_memberships.user_id = auth.uid()
|
||||
group by
|
||||
accounts.id,
|
||||
accounts_memberships.account_role,
|
||||
subscriptions.status;
|
||||
subscriptions.status,
|
||||
roles.hierarchy_level;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
@@ -1324,7 +1371,8 @@ OR REPLACE FUNCTION public.get_account_members (account_slug text) RETURNS TABLE
|
||||
id uuid,
|
||||
user_id uuid,
|
||||
account_id uuid,
|
||||
role public.account_role,
|
||||
role varchar(50),
|
||||
role_hierarchy_level int,
|
||||
primary_owner_user_id uuid,
|
||||
name varchar,
|
||||
email varchar,
|
||||
@@ -1334,10 +1382,11 @@ OR REPLACE FUNCTION public.get_account_members (account_slug text) RETURNS TABLE
|
||||
) LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT acc.id, am.user_id, am.account_id, am.account_role, a.primary_owner_user_id, acc.name, acc.email, acc.picture_url, am.created_at, am.updated_at
|
||||
SELECT acc.id, am.user_id, am.account_id, am.account_role, r.hierarchy_level, a.primary_owner_user_id, acc.name, acc.email, acc.picture_url, am.created_at, am.updated_at
|
||||
FROM public.accounts_memberships am
|
||||
JOIN public.accounts a ON a.id = am.account_id
|
||||
JOIN public.accounts acc on acc.id = am.user_id
|
||||
JOIN public.roles r ON r.name = am.account_role
|
||||
WHERE a.slug = account_slug;
|
||||
END;
|
||||
$$;
|
||||
@@ -1351,7 +1400,7 @@ create or replace function public.get_account_invitations(account_slug text) ret
|
||||
email varchar(255),
|
||||
account_id uuid,
|
||||
invited_by uuid,
|
||||
role public.account_role,
|
||||
role varchar(50),
|
||||
created_at timestamptz,
|
||||
updated_at timestamptz,
|
||||
expires_at timestamptz,
|
||||
@@ -1383,7 +1432,7 @@ grant execute on function public.get_account_invitations (text) to authenticated
|
||||
|
||||
CREATE TYPE kit.invitation AS (
|
||||
email text,
|
||||
role public.account_role
|
||||
role varchar(50)
|
||||
);
|
||||
|
||||
-- Then, modify your function to use this type
|
||||
@@ -1394,7 +1443,7 @@ DECLARE
|
||||
all_invitations public.invitations[] := ARRAY[]::public.invitations[];
|
||||
invite_token text;
|
||||
email text;
|
||||
role public.account_role;
|
||||
role varchar(50);
|
||||
BEGIN
|
||||
FOREACH email, role IN ARRAY invitations
|
||||
LOOP
|
||||
|
||||
Reference in New Issue
Block a user