From 0d0719e8f9ffa0d4ee85ab2fd116fe0055cd2486 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Fri, 8 Aug 2025 04:12:46 +0100 Subject: [PATCH] feat: implement row-level security policies for course-related tables and actions --- package.json | 5 +- .../20250808030821_rls_secure_courses.sql | 390 ++++++++++++++++++ supabase/seed.sql | 2 +- 3 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 supabase/migrations/20250808030821_rls_secure_courses.sql diff --git a/package.json b/package.json index 02a6c6e3..b96ecf7b 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,7 @@ "format": "prettier . --write", "release": "standard-version", "ci": "cypress run", - "supabase:start": "pnpx supabase@1.51.0 start", - "supabase:login": "pnpx supabase@1.51.0 login", - "supabase:status": "pnpx supabase@1.51.0 status", - "supabase": "pnpx supabase@1.51.0", + "supabase": "pnpx supabase", "dashboard:build": "cd apps/dashboard && pnpm i && IS_SELFHOSTED=true NODE_OPTIONS='--max-old-space-size=20384' DEPLOYMENT_PROVIDER=docker pnpm build", "dashboard:start": "cd apps/dashboard && node build" }, diff --git a/supabase/migrations/20250808030821_rls_secure_courses.sql b/supabase/migrations/20250808030821_rls_secure_courses.sql new file mode 100644 index 00000000..ef94c83e --- /dev/null +++ b/supabase/migrations/20250808030821_rls_secure_courses.sql @@ -0,0 +1,390 @@ +drop policy "Enable access to all users" on "public"."course"; + +drop policy "User must be an course member to INSERT" on "public"."lesson_completion"; + +drop policy "User must be a course member to INSERT" on "public"."apps_poll"; + +drop policy "User must be course member to SELECT" on "public"."apps_poll"; + +drop policy "User must be a course member to INSERT" on "public"."apps_poll_option"; + +drop policy "User must be a course member to UPDATE" on "public"."apps_poll_option"; + +drop policy "User must be a teacher to DELETE" on "public"."apps_poll_option"; + +drop policy "User must be course member to SELECT" on "public"."apps_poll_option"; + +drop policy "User must be a course member to INSERT" on "public"."course_newsfeed"; + +drop policy "User must be a course member to SELECT" on "public"."course_newsfeed"; + +drop policy "User must be a course member to INSERT" on "public"."course_newsfeed_comment"; + +drop policy "User must be a course member to SELECT" on "public"."course_newsfeed_comment"; + +drop policy "User must be a course member to INSERT" on "public"."group_attendance"; + +drop policy "User must be a course member to SELECT" on "public"."group_attendance"; + +drop policy "User must be a course member to UPDATE" on "public"."group_attendance"; + +drop policy "User must be in course group to INSERT" on "public"."lesson_comment"; + +drop policy "User must be an course member to DELETE" on "public"."question_answer"; + +drop policy "User must be an course member to INSERT" on "public"."question_answer"; + +drop policy "User must be an course member to UPDATE" on "public"."question_answer"; + +drop policy "User must be a course member to DELETE" on "public"."submission"; + +drop policy "User must be a course member to INSERT" on "public"."submission"; + +drop policy "User must be a course member to UPDATE" on "public"."submission"; + +drop function if exists "public"."is_user_in_group_with_role"(group_id integer); + +set check_function_bodies = off; + +CREATE OR REPLACE FUNCTION public.is_user_in_course_group_or_admin(group_id uuid) + RETURNS boolean + LANGUAGE plpgsql + SECURITY DEFINER +AS $function$ +DECLARE + org_id uuid; + is_admin boolean; + is_member boolean; +BEGIN + SELECT organization_id INTO org_id FROM "group" WHERE id = $1 LIMIT 1; + is_admin := is_org_admin(org_id); + is_member := is_user_in_course_group($1); + RETURN is_admin OR is_member; +END; +$function$ +; + +create policy "Enable access to all users if PUBLIC or to course members when " +on "public"."course" +as permissive +for select +to public +using ((is_published OR is_user_in_course_group_or_admin(group_id))); + + +create policy "User must be an course member or Admin to perform ALL operation" +on "public"."lesson_completion" +as permissive +for all +to public +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = ( SELECT lesson.course_id + FROM lesson + WHERE (lesson.id = lesson_completion.lesson_id) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be an admin to INSERT or allow if no existing member" +on "public"."organizationmember" +as permissive +for insert +to public +with check ((is_org_admin() OR (NOT (EXISTS ( SELECT 1 + FROM organizationmember organizationmember_1 + WHERE (organizationmember_1.organization_id = organizationmember_1.organization_id)))))); + + +create policy "User must be a course member to INSERT" +on "public"."apps_poll" +as permissive +for insert +to public +with check (is_user_in_course_group_or_admin(( SELECT groupmember.group_id + FROM groupmember + WHERE (groupmember.id = apps_poll."authorId") + LIMIT 1))); + + +create policy "User must be course member to SELECT" +on "public"."apps_poll" +as permissive +for select +to public +using (is_user_in_course_group_or_admin(( SELECT groupmember.group_id + FROM groupmember + WHERE (groupmember.id = apps_poll."authorId") + LIMIT 1))); + + +create policy "User must be a course member to INSERT" +on "public"."apps_poll_option" +as permissive +for insert +to public +with check (is_user_in_course_group_or_admin(( SELECT groupmember.group_id + FROM groupmember + WHERE (groupmember.id = ( SELECT apps_poll."authorId" + FROM apps_poll + WHERE (apps_poll.id = apps_poll_option.poll_id) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be a course member to UPDATE" +on "public"."apps_poll_option" +as permissive +for update +to public +using (is_user_in_course_group_or_admin(( SELECT groupmember.group_id + FROM groupmember + WHERE (groupmember.id = ( SELECT apps_poll."authorId" + FROM apps_poll + WHERE (apps_poll.id = apps_poll_option.poll_id) + LIMIT 1)) + LIMIT 1))) +with check (is_user_in_course_group_or_admin(( SELECT groupmember.group_id + FROM groupmember + WHERE (groupmember.id = ( SELECT apps_poll."authorId" + FROM apps_poll + WHERE (apps_poll.id = apps_poll_option.poll_id) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be a teacher to DELETE" +on "public"."apps_poll_option" +as permissive +for delete +to public +using (is_user_in_course_group_or_admin(( SELECT groupmember.group_id + FROM groupmember + WHERE (groupmember.id = ( SELECT apps_poll."authorId" + FROM apps_poll + WHERE (apps_poll.id = apps_poll_option.poll_id) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be course member to SELECT" +on "public"."apps_poll_option" +as permissive +for select +to public +using (is_user_in_course_group_or_admin(( SELECT groupmember.group_id + FROM groupmember + WHERE (groupmember.id = ( SELECT apps_poll."authorId" + FROM apps_poll + WHERE (apps_poll.id = apps_poll_option.poll_id) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be a course member to INSERT" +on "public"."course_newsfeed" +as permissive +for insert +to public +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = course_newsfeed.course_id) + LIMIT 1))); + + +create policy "User must be a course member to SELECT" +on "public"."course_newsfeed" +as permissive +for select +to public +using (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = course_newsfeed.course_id) + LIMIT 1))); + + +create policy "User must be a course member to INSERT" +on "public"."course_newsfeed_comment" +as permissive +for insert +to public +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = ( SELECT course_newsfeed.course_id + FROM course_newsfeed + WHERE (course_newsfeed.id = course_newsfeed_comment.course_newsfeed_id))) + LIMIT 1))); + + +create policy "User must be a course member to SELECT" +on "public"."course_newsfeed_comment" +as permissive +for select +to public +using (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = ( SELECT course_newsfeed.course_id + FROM course_newsfeed + WHERE (course_newsfeed.id = course_newsfeed_comment.course_newsfeed_id))) + LIMIT 1))); + + +create policy "User must be a course member to INSERT" +on "public"."group_attendance" +as permissive +for insert +to public +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = group_attendance.course_id) + LIMIT 1))); + + +create policy "User must be a course member to SELECT" +on "public"."group_attendance" +as permissive +for select +to public +using (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = group_attendance.course_id) + LIMIT 1))); + + +create policy "User must be a course member to UPDATE" +on "public"."group_attendance" +as permissive +for update +to public +using (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = group_attendance.course_id) + LIMIT 1))) +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = group_attendance.course_id) + LIMIT 1))); + + +create policy "User must be in course group to INSERT" +on "public"."lesson_comment" +as permissive +for insert +to public +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = ( SELECT lesson.course_id + FROM lesson + WHERE (lesson.id = lesson_comment.lesson_id) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be an course member to DELETE" +on "public"."question_answer" +as permissive +for delete +to public +using (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = ( SELECT lesson.course_id + FROM lesson + WHERE (lesson.id = ( SELECT exercise.lesson_id + FROM exercise + WHERE (exercise.id = ( SELECT question.exercise_id + FROM question + WHERE (question.id = question_answer.question_id) + LIMIT 1)) + LIMIT 1)) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be an course member to INSERT" +on "public"."question_answer" +as permissive +for insert +to public +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = ( SELECT lesson.course_id + FROM lesson + WHERE (lesson.id = ( SELECT exercise.lesson_id + FROM exercise + WHERE (exercise.id = ( SELECT question.exercise_id + FROM question + WHERE (question.id = question_answer.question_id) + LIMIT 1)) + LIMIT 1)) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be an course member to UPDATE" +on "public"."question_answer" +as permissive +for update +to public +using (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = ( SELECT lesson.course_id + FROM lesson + WHERE (lesson.id = ( SELECT exercise.lesson_id + FROM exercise + WHERE (exercise.id = ( SELECT question.exercise_id + FROM question + WHERE (question.id = question_answer.question_id) + LIMIT 1)) + LIMIT 1)) + LIMIT 1)) + LIMIT 1))) +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = ( SELECT lesson.course_id + FROM lesson + WHERE (lesson.id = ( SELECT exercise.lesson_id + FROM exercise + WHERE (exercise.id = ( SELECT question.exercise_id + FROM question + WHERE (question.id = question_answer.question_id) + LIMIT 1)) + LIMIT 1)) + LIMIT 1)) + LIMIT 1))); + + +create policy "User must be a course member to DELETE" +on "public"."submission" +as permissive +for delete +to public +using (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = submission.course_id) + LIMIT 1))); + + +create policy "User must be a course member to INSERT" +on "public"."submission" +as permissive +for insert +to public +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = submission.course_id) + LIMIT 1))); + + +create policy "User must be a course member to UPDATE" +on "public"."submission" +as permissive +for update +to public +using (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = submission.course_id) + LIMIT 1))) +with check (is_user_in_course_group_or_admin(( SELECT course.group_id + FROM course + WHERE (course.id = submission.course_id) + LIMIT 1))); diff --git a/supabase/seed.sql b/supabase/seed.sql index f3a0bf64..0cebf36c 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -19,7 +19,7 @@ INSERT INTO "auth"."users" ("instance_id", "id", "aud", "role", "email", "encryp -- Data for Name: identities; Type: TABLE DATA; Schema: auth; Owner: supabase_auth_admin -- -INSERT INTO "auth"."identities" ("id", "user_id", "identity_data", "provider", "last_sign_in_at", "created_at", "updated_at") VALUES +INSERT INTO "auth"."identities" ("provider_id", "user_id", "identity_data", "provider", "last_sign_in_at", "created_at", "updated_at") VALUES ('01676a50-bb56-4c5e-8a61-fb9e9190fb10', '01676a50-bb56-4c5e-8a61-fb9e9190fb10', '{"sub": "01676a50-bb56-4c5e-8a61-fb9e9190fb10", "email": "test@test.com"}', 'email', '2023-11-22 10:09:32.225444+00', '2023-11-22 10:09:32.225521+00', '2023-11-22 10:09:32.225521+00'), ('7ac00503-8519-43c8-a5ea-b79aeca900b1', '7ac00503-8519-43c8-a5ea-b79aeca900b1', '{"sub": "7ac00503-8519-43c8-a5ea-b79aeca900b1", "email": "admin@test.com"}', 'email', '2023-12-16 14:04:38.302264+00', '2023-12-16 14:04:38.302326+00', '2023-12-16 14:04:38.302326+00'), ('0c256e75-aa40-4f62-8d30-0217ca1c60d9', '0c256e75-aa40-4f62-8d30-0217ca1c60d9', '{"sub": "0c256e75-aa40-4f62-8d30-0217ca1c60d9", "email": "student@test.com"}', 'email', '2023-12-18 17:08:57.410285+00', '2023-12-18 17:08:57.410344+00', '2023-12-18 17:08:57.410344+00');