-- 1. Enable RLS on all tables DO $$ BEGIN ALTER TABLE "Admin" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Student" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Teacher" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Parent" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Grade" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Class" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Subject" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Lesson" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Exam" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Assignment" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Result" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Attendance" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Event" ENABLE ROW LEVEL SECURITY; ALTER TABLE "Announcement" ENABLE ROW LEVEL SECURITY; ALTER TABLE "_SubjectToTeacher" ENABLE ROW LEVEL SECURITY; EXCEPTION WHEN undefined_table THEN NULL; END $$; -- 2. Utility helper functions connecting to Clerk JWT CREATE OR REPLACE FUNCTION requesting_user_id() RETURNS text LANGUAGE sql STABLE AS $$ SELECT NULLIF(current_setting('request.jwt.claims', true)::json->>'sub', '')::text; $$; CREATE OR REPLACE FUNCTION requesting_user_role() RETURNS text LANGUAGE sql STABLE AS $$ SELECT current_setting('request.jwt.claims', true)::json->>'role'; $$; CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean LANGUAGE sql STABLE AS $$ SELECT requesting_user_role() = 'admin'; $$; -- 3. Bypass-RLS Helper Functions for finding accessible IDs -- We use SECURITY DEFINER SET search_path = public to execute these queries as the database owner, -- which prevents infinite recursion errors (stack depth limit exceeded) when querying tables that have RLS enabled. -- Transaction-local variables like `request.jwt.claims` perfectly persist through SECURITY DEFINER calls. CREATE OR REPLACE FUNCTION auth_user_classes() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT "classId" FROM "Student" WHERE id = requesting_user_id() AND requesting_user_role() = 'student' UNION SELECT "classId" FROM "Student" WHERE "parentId" = requesting_user_id() AND requesting_user_role() = 'parent' UNION SELECT "classId" FROM "Lesson" WHERE "teacherId" = requesting_user_id() AND requesting_user_role() = 'teacher' UNION SELECT id FROM "Class" WHERE "supervisorId" = requesting_user_id() AND requesting_user_role() = 'teacher'; END; $$; CREATE OR REPLACE FUNCTION auth_user_students() RETURNS SETOF text LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT id FROM "Student" WHERE id = requesting_user_id() AND requesting_user_role() = 'student' UNION SELECT id FROM "Student" WHERE "parentId" = requesting_user_id() AND requesting_user_role() = 'parent' UNION SELECT id FROM "Student" WHERE "classId" IN ( SELECT "classId" FROM "Lesson" WHERE "teacherId" = requesting_user_id() ) AND requesting_user_role() = 'teacher'; END; $$; CREATE OR REPLACE FUNCTION auth_user_lessons() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT id FROM "Lesson" WHERE "teacherId" = requesting_user_id() AND requesting_user_role() = 'teacher' UNION SELECT id FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "Student" WHERE id = requesting_user_id() AND requesting_user_role() = 'student' ) UNION SELECT id FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "Student" WHERE "parentId" = requesting_user_id() AND requesting_user_role() = 'parent' ); END; $$; CREATE OR REPLACE FUNCTION auth_user_subjects() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT "A" FROM "_SubjectToTeacher" WHERE "B" = requesting_user_id() AND requesting_user_role() = 'teacher' UNION SELECT "subjectId" FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "Student" WHERE id = requesting_user_id() AND requesting_user_role() = 'student' ) UNION SELECT "subjectId" FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "Student" WHERE "parentId" = requesting_user_id() AND requesting_user_role() = 'parent' ); END; $$; CREATE OR REPLACE FUNCTION auth_user_teachers() RETURNS SETOF text LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT id FROM "Teacher" WHERE id = requesting_user_id() AND requesting_user_role() = 'teacher' UNION SELECT "teacherId" FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "Student" WHERE id = requesting_user_id() AND requesting_user_role() = 'student' ) UNION SELECT "teacherId" FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "Student" WHERE "parentId" = requesting_user_id() AND requesting_user_role() = 'parent' ); END; $$; CREATE OR REPLACE FUNCTION auth_user_parents() RETURNS SETOF text LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT id FROM "Parent" WHERE id = requesting_user_id() AND requesting_user_role() = 'parent' UNION SELECT "parentId" FROM "Student" WHERE id = requesting_user_id() AND requesting_user_role() = 'student'; END; $$; -- 4. Define Table Policies using Helpers CREATE POLICY "Admin has full access" ON "Admin" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Student" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Teacher" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Parent" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Grade" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Class" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Subject" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Lesson" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Exam" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Assignment" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Result" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Attendance" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Event" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "Announcement" FOR ALL USING (is_admin()); CREATE POLICY "Admins have full access" ON "_SubjectToTeacher" FOR ALL USING (is_admin()); CREATE POLICY "Anyone can view grades" ON "Grade" FOR SELECT TO authenticated USING (true); CREATE POLICY "Anyone can view subject-teacher" ON "_SubjectToTeacher" FOR SELECT TO authenticated USING (true); CREATE POLICY "Users can view permitted students" ON "Student" FOR SELECT USING ( is_admin() OR id IN (SELECT auth_user_students()) ); CREATE POLICY "Users can view permitted teachers" ON "Teacher" FOR SELECT USING ( is_admin() OR id IN (SELECT auth_user_teachers()) ); CREATE POLICY "Users can view permitted parents" ON "Parent" FOR SELECT USING ( is_admin() OR id IN (SELECT auth_user_parents()) ); CREATE POLICY "Users can view permitted subjects" ON "Subject" FOR SELECT USING ( is_admin() OR id IN (SELECT auth_user_subjects()) ); CREATE POLICY "Users can view permitted classes" ON "Class" FOR SELECT USING ( is_admin() OR id IN (SELECT auth_user_classes()) ); CREATE POLICY "Users can view permitted lessons" ON "Lesson" FOR SELECT USING ( is_admin() OR id IN (SELECT auth_user_lessons()) ); CREATE POLICY "Users can view permitted exams" ON "Exam" FOR SELECT USING ( is_admin() OR "lessonId" IN (SELECT auth_user_lessons()) ); CREATE POLICY "Users can view permitted assignments" ON "Assignment" FOR SELECT USING ( is_admin() OR "lessonId" IN (SELECT auth_user_lessons()) ); CREATE POLICY "Users can view permitted results" ON "Result" FOR SELECT USING ( is_admin() OR "studentId" IN (SELECT auth_user_students()) OR ( requesting_user_role() = 'teacher' AND ( "examId" IN (SELECT id FROM "Exam" WHERE "lessonId" IN (SELECT auth_user_lessons())) OR "assignmentId" IN (SELECT id FROM "Assignment" WHERE "lessonId" IN (SELECT auth_user_lessons())) ) ) ); CREATE POLICY "Users can view permitted attendance" ON "Attendance" FOR SELECT USING ( is_admin() OR "studentId" IN (SELECT auth_user_students()) ); CREATE POLICY "Users can view permitted events" ON "Event" FOR SELECT USING ( is_admin() OR "classId" IS NULL OR "classId" IN (SELECT auth_user_classes()) ); CREATE POLICY "Users can view permitted announcements" ON "Announcement" FOR SELECT USING ( is_admin() OR "classId" IS NULL OR "classId" IN (SELECT auth_user_classes()) );