From 6e5cc0086e07158e06899112dda6eae9d2ad6176 Mon Sep 17 00:00:00 2001 From: easy-street <64047794+easy-street@users.noreply.github.com> Date: Sun, 16 May 2021 22:05:19 -0500 Subject: [PATCH] feat: extend nextjs-slack-clone with RBAC --- examples/nextjs-slack-clone/.gitignore | 3 + examples/nextjs-slack-clone/README.md | 39 ++++++++++- .../nextjs-slack-clone/components/Layout.js | 21 ++++-- .../nextjs-slack-clone/components/Message.js | 31 +++++++-- .../components/TrashIcon.js | 25 +++++++ examples/nextjs-slack-clone/lib/Store.js | 67 +++++++++++++++++-- examples/nextjs-slack-clone/pages/_app.js | 53 +++++++-------- .../nextjs-slack-clone/pages/channels/[id].js | 7 ++ examples/nextjs-slack-clone/pages/index.js | 4 +- 9 files changed, 198 insertions(+), 52 deletions(-) create mode 100644 examples/nextjs-slack-clone/components/TrashIcon.js diff --git a/examples/nextjs-slack-clone/.gitignore b/examples/nextjs-slack-clone/.gitignore index 141ec63810c..acc077f8823 100644 --- a/examples/nextjs-slack-clone/.gitignore +++ b/examples/nextjs-slack-clone/.gitignore @@ -24,3 +24,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# Supabase +.supabase diff --git a/examples/nextjs-slack-clone/README.md b/examples/nextjs-slack-clone/README.md index 8d5fbafd70a..6b6cd67822e 100644 --- a/examples/nextjs-slack-clone/README.md +++ b/examples/nextjs-slack-clone/README.md @@ -114,6 +114,17 @@ Visit http://localhost:3000 and start chatting! Open a channel across two browse ## Supabase details +### Role-based access control (RBAC) +Use [plus addressing](https://en.wikipedia.org/wiki/Email_address#Subaddressing) to sign up users with the `admin` & `moderator` roles. Email addresses including `+supaadmin@` will be assigned the `admin` role, and email addresses including `+supamod@` will be assigned the `moderator` role. For example: +``` +// admin user +email+supaadmin@example.com + +// moderator user +email+supamod@example.com +``` +Users with the `moderator` role can delete all messages. Users with the `admin` role can delete all messages and channels (note: it's not recommended to delete the `public` channel). + ### Postgres Row level security This project uses very high-level Authorization using Postgres' Role Level Security. @@ -124,6 +135,26 @@ We can use these details to provide fine-grained control over what each user can This is a trimmed-down schema, with the policies: ```sql +-- authorize with role-based access control (RBAC) +CREATE FUNCTION public.authorize( + requested_permission app_permission, + user_id uuid +) +RETURNS BOOLEAN AS $$ +DECLARE + bind_permissions int; +BEGIN + SELECT count(*) + FROM public.role_permissions + INNER JOIN public.user_roles on role_permissions.role = user_roles.role + WHERE role_permissions.permission = authorize.requested_permission + AND user_roles.user_id = authorize.user_id + INTO bind_permissions; + + RETURN bind_permissions > 0; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + -- USER PROFILES CREATE TYPE public.user_status AS ENUM ('ONLINE', 'OFFLINE'); CREATE TABLE public.users ( @@ -143,7 +174,11 @@ CREATE TABLE public.channels ( slug text NOT NULL UNIQUE ); ALTER TABLE public.channels ENABLE ROW LEVEL SECURITY; -CREATE POLICY "Allow logged-in full access" on public.channels FOR ALL USING ( auth.role() = 'authenticated' ); +CREATE POLICY "Allow logged-in read access" on public.channels FOR SELECT USING ( auth.role() = 'authenticated' ); +CREATE POLICY "Allow individual insert access" on public.channels FOR INSERT WITH CHECK ( auth.uid() = created_by ); +CREATE POLICY "Allow individual update access" on public.channels FOR UPDATE USING ( auth.uid() = created_by ); +CREATE POLICY "Allow individual delete access" on public.channels FOR DELETE USING ( auth.uid() = created_by ); +CREATE POLICY "Allow authorized delete access" on public.channels FOR DELETE USING ( authorize('channels.delete', auth.uid()) ); -- MESSAGES CREATE TABLE public.messages ( @@ -157,6 +192,8 @@ ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY; CREATE POLICY "Allow logged-in read access" on public.messages FOR SELECT USING ( auth.role() = 'authenticated' ); CREATE POLICY "Allow individual insert access" on public.messages FOR INSERT WITH CHECK ( auth.uid() = user_id ); CREATE POLICY "Allow individual update access" on public.messages FOR UPDATE USING ( auth.uid() = user_id ); +CREATE POLICY "Allow individual delete access" on public.messages FOR DELETE USING ( auth.uid() = user_id ); +CREATE POLICY "Allow authorized delete access" on public.messages FOR DELETE USING ( authorize('messages.delete', auth.uid()) ); ``` ## Authors diff --git a/examples/nextjs-slack-clone/components/Layout.js b/examples/nextjs-slack-clone/components/Layout.js index 1e05dd399da..a592e0d83be 100644 --- a/examples/nextjs-slack-clone/components/Layout.js +++ b/examples/nextjs-slack-clone/components/Layout.js @@ -1,10 +1,11 @@ import Link from 'next/link' import { useContext } from 'react' import UserContext from '~/lib/UserContext' -import { addChannel } from '~/lib/Store' +import { addChannel, deleteChannel } from '~/lib/Store' +import TrashIcon from '~/components/TrashIcon' export default function Layout(props) { - const { signOut } = useContext(UserContext) + const { signOut, user, userRoles } = useContext(UserContext) const slugify = (text) => { return text @@ -20,7 +21,7 @@ export default function Layout(props) { const newChannel = async () => { const slug = prompt('Please enter your name') if (slug) { - addChannel(slugify(slug)) + addChannel(slugify(slug), user.id) } } @@ -41,7 +42,8 @@ export default function Layout(props) {