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:
giancarlo
2024-03-29 14:48:45 +08:00
parent f1967a686c
commit 99db8f4ca4
41 changed files with 498 additions and 228 deletions

View File

@@ -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