mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -05:00
Fix: Implement MFA enforcement and critical bug fix
This commit is contained in:
@@ -2,6 +2,9 @@ import { ReactNode } from 'react';
|
|||||||
import { SidebarProvider } from '@/components/ui/sidebar';
|
import { SidebarProvider } from '@/components/ui/sidebar';
|
||||||
import { AdminSidebar } from './AdminSidebar';
|
import { AdminSidebar } from './AdminSidebar';
|
||||||
import { AdminTopBar } from './AdminTopBar';
|
import { AdminTopBar } from './AdminTopBar';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
import { useSessionMonitor } from '@/hooks/useSessionMonitor';
|
||||||
|
|
||||||
interface AdminLayoutProps {
|
interface AdminLayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -20,6 +23,8 @@ export function AdminLayout({
|
|||||||
lastUpdated,
|
lastUpdated,
|
||||||
isRefreshing
|
isRefreshing
|
||||||
}: AdminLayoutProps) {
|
}: AdminLayoutProps) {
|
||||||
|
const { aalWarning } = useSessionMonitor();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider defaultOpen={true}>
|
<SidebarProvider defaultOpen={true}>
|
||||||
<div className="flex min-h-screen w-full">
|
<div className="flex min-h-screen w-full">
|
||||||
@@ -34,6 +39,15 @@ export function AdminLayout({
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1 overflow-y-auto bg-muted/30">
|
<div className="flex-1 overflow-y-auto bg-muted/30">
|
||||||
<div className="container mx-auto px-6 py-8 max-w-7xl">
|
<div className="container mx-auto px-6 py-8 max-w-7xl">
|
||||||
|
{aalWarning && (
|
||||||
|
<Alert variant="destructive" className="mb-6">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertTitle>Session Verification Required</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Your session requires re-verification. You will be redirected to verify your identity in 30 seconds.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,6 +29,31 @@ export function AccountDeletionDialog({ open, onOpenChange, userEmail, onDeletio
|
|||||||
const handleRequestDeletion = async () => {
|
const handleRequestDeletion = async () => {
|
||||||
if (!canRequestDeletion(state)) return;
|
if (!canRequestDeletion(state)) return;
|
||||||
|
|
||||||
|
// Phase 4: AAL2 check for security-critical operations
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
if (session) {
|
||||||
|
// Check if user has MFA enrolled
|
||||||
|
const { data: factorsData } = await supabase.auth.mfa.listFactors();
|
||||||
|
const hasMFA = factorsData?.totp?.some(f => f.status === 'verified') || false;
|
||||||
|
|
||||||
|
if (hasMFA) {
|
||||||
|
const jwt = session.access_token;
|
||||||
|
const payload = JSON.parse(atob(jwt.split('.')[1]));
|
||||||
|
const currentAal = payload.aal || 'aal1';
|
||||||
|
|
||||||
|
if (currentAal !== 'aal2') {
|
||||||
|
handleError(
|
||||||
|
new Error('Please verify your identity with MFA first'),
|
||||||
|
{ action: 'Request account deletion' }
|
||||||
|
);
|
||||||
|
sessionStorage.setItem('mfa_step_up_required', 'true');
|
||||||
|
sessionStorage.setItem('mfa_intended_path', '/settings?tab=privacy');
|
||||||
|
window.location.href = '/auth';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({ type: 'SET_LOADING', payload: true });
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -95,6 +95,34 @@ export function EmailChangeDialog({ open, onOpenChange, currentEmail, userId }:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 4: AAL2 check for security-critical operations
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
if (session) {
|
||||||
|
// Check if user has MFA enrolled
|
||||||
|
const { data: factorsData } = await supabase.auth.mfa.listFactors();
|
||||||
|
const hasMFA = factorsData?.totp?.some(f => f.status === 'verified') || false;
|
||||||
|
|
||||||
|
if (hasMFA) {
|
||||||
|
const jwt = session.access_token;
|
||||||
|
const payload = JSON.parse(atob(jwt.split('.')[1]));
|
||||||
|
const currentAal = payload.aal || 'aal1';
|
||||||
|
|
||||||
|
if (currentAal !== 'aal2') {
|
||||||
|
handleError(
|
||||||
|
new AppError(
|
||||||
|
'Please verify your identity with MFA first',
|
||||||
|
'AAL2_REQUIRED'
|
||||||
|
),
|
||||||
|
{ action: 'Change email', userId, metadata: { step: 'aal2_check' } }
|
||||||
|
);
|
||||||
|
sessionStorage.setItem('mfa_step_up_required', 'true');
|
||||||
|
sessionStorage.setItem('mfa_intended_path', '/settings?tab=security');
|
||||||
|
window.location.href = '/auth';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Step 1: Validate email is not disposable
|
// Step 1: Validate email is not disposable
|
||||||
|
|||||||
@@ -101,6 +101,30 @@ export function PasswordUpdateDialog({ open, onOpenChange, onSuccess }: Password
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 4: AAL2 check for security-critical operations
|
||||||
|
if (hasMFA) {
|
||||||
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
if (session) {
|
||||||
|
const jwt = session.access_token;
|
||||||
|
const payload = JSON.parse(atob(jwt.split('.')[1]));
|
||||||
|
const currentAal = payload.aal || 'aal1';
|
||||||
|
|
||||||
|
if (currentAal !== 'aal2') {
|
||||||
|
handleError(
|
||||||
|
new AppError(
|
||||||
|
'Please verify your identity with MFA first',
|
||||||
|
'AAL2_REQUIRED'
|
||||||
|
),
|
||||||
|
{ action: 'Change password', userId, metadata: { step: 'aal2_check' } }
|
||||||
|
);
|
||||||
|
sessionStorage.setItem('mfa_step_up_required', 'true');
|
||||||
|
sessionStorage.setItem('mfa_intended_path', '/settings?tab=security');
|
||||||
|
window.location.href = '/auth';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Step 1: Reauthenticate with current password to get a nonce
|
// Step 1: Reauthenticate with current password to get a nonce
|
||||||
|
|||||||
74
src/hooks/useSessionMonitor.ts
Normal file
74
src/hooks/useSessionMonitor.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useAuth } from './useAuth';
|
||||||
|
import { useRequireMFA } from './useRequireMFA';
|
||||||
|
import { getSessionAal } from '@/lib/authService';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3: Session Monitoring Hook
|
||||||
|
* Monitors AAL degradation and forces re-verification when needed
|
||||||
|
*
|
||||||
|
* This hook continuously checks the session's AAL level and detects
|
||||||
|
* if it degrades from AAL2 to AAL1, which can happen after token refresh
|
||||||
|
* or session expiry.
|
||||||
|
*/
|
||||||
|
export function useSessionMonitor() {
|
||||||
|
const { aal, session, user } = useAuth();
|
||||||
|
const { requiresMFA, isEnrolled } = useRequireMFA();
|
||||||
|
const [aalWarning, setAalWarning] = useState(false);
|
||||||
|
const [aalDegraded, setAalDegraded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session || !user || !requiresMFA || !isEnrolled) {
|
||||||
|
setAalWarning(false);
|
||||||
|
setAalDegraded(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check AAL every 60 seconds
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const currentAal = await getSessionAal(session);
|
||||||
|
|
||||||
|
// If AAL degraded from AAL2 to AAL1
|
||||||
|
if (currentAal === 'aal1' && aal === 'aal2') {
|
||||||
|
logger.warn('AAL degradation detected', {
|
||||||
|
userId: user.id,
|
||||||
|
previousAal: aal,
|
||||||
|
currentAal,
|
||||||
|
action: 'session_monitor'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show warning for 30 seconds
|
||||||
|
setAalWarning(true);
|
||||||
|
setAalDegraded(true);
|
||||||
|
|
||||||
|
// After 30 seconds, redirect to MFA step-up
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.info('Forcing MFA step-up due to AAL degradation', {
|
||||||
|
userId: user.id,
|
||||||
|
action: 'session_monitor_redirect'
|
||||||
|
});
|
||||||
|
|
||||||
|
sessionStorage.setItem('mfa_step_up_required', 'true');
|
||||||
|
sessionStorage.setItem('mfa_intended_path', window.location.pathname);
|
||||||
|
window.location.href = '/auth';
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Session monitor check failed', {
|
||||||
|
userId: user.id,
|
||||||
|
action: 'session_monitor',
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [session, aal, requiresMFA, isEnrolled, user]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
aalWarning,
|
||||||
|
aalDegraded
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -115,6 +115,39 @@ serve(async (req) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2: AAL2 Enforcement - Check if user has MFA enrolled and requires AAL2
|
||||||
|
const { data: { session } } = await supabaseAuth.auth.getSession();
|
||||||
|
if (!session) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'No active session found.' }),
|
||||||
|
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user has MFA enrolled
|
||||||
|
const { data: factorsData } = await supabaseAuth.auth.mfa.listFactors();
|
||||||
|
const hasMFA = factorsData?.totp?.some(f => f.status === 'verified') || false;
|
||||||
|
|
||||||
|
// Parse JWT to get AAL level
|
||||||
|
const jwt = session.access_token;
|
||||||
|
const payload = JSON.parse(atob(jwt.split('.')[1]));
|
||||||
|
const aal = payload.aal || 'aal1';
|
||||||
|
|
||||||
|
// Enforce AAL2 if MFA is enrolled
|
||||||
|
if (hasMFA && aal !== 'aal2') {
|
||||||
|
console.error('AAL2 required but session is at AAL1', { userId: authenticatedUserId });
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'MFA verification required',
|
||||||
|
code: 'AAL2_REQUIRED',
|
||||||
|
message: 'Your role requires two-factor authentication. Please verify your identity to continue.'
|
||||||
|
}),
|
||||||
|
{ status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('AAL2 check passed', { userId: authenticatedUserId, hasMFA, aal });
|
||||||
|
|
||||||
const { itemIds, submissionId }: ApprovalRequest = await req.json();
|
const { itemIds, submissionId }: ApprovalRequest = await req.json();
|
||||||
|
|
||||||
// UUID validation regex
|
// UUID validation regex
|
||||||
|
|||||||
@@ -0,0 +1,349 @@
|
|||||||
|
-- Phase 0: Fix critical bug - Users can't create submissions
|
||||||
|
-- Add missing INSERT policy for submission_items
|
||||||
|
|
||||||
|
CREATE POLICY "Users can insert their own submission items"
|
||||||
|
ON public.submission_items
|
||||||
|
FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM content_submissions cs
|
||||||
|
WHERE cs.id = submission_items.submission_id
|
||||||
|
AND cs.user_id = auth.uid()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Phase 1: Complete Database-Level AAL2 Enforcement
|
||||||
|
-- Add AAL2 checks to all moderator-accessible tables
|
||||||
|
|
||||||
|
-- 1.1 Submission Review Tables
|
||||||
|
|
||||||
|
-- park_submissions
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update park submissions" ON public.park_submissions;
|
||||||
|
CREATE POLICY "Moderators can update park submissions"
|
||||||
|
ON public.park_submissions
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete park submissions" ON public.park_submissions;
|
||||||
|
CREATE POLICY "Moderators can delete park submissions"
|
||||||
|
ON public.park_submissions
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all park submissions" ON public.park_submissions;
|
||||||
|
CREATE POLICY "Moderators can view all park submissions"
|
||||||
|
ON public.park_submissions
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ride_submissions
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update ride submissions" ON public.ride_submissions;
|
||||||
|
CREATE POLICY "Moderators can update ride submissions"
|
||||||
|
ON public.ride_submissions
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete ride submissions" ON public.ride_submissions;
|
||||||
|
CREATE POLICY "Moderators can delete ride submissions"
|
||||||
|
ON public.ride_submissions
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all ride submissions" ON public.ride_submissions;
|
||||||
|
CREATE POLICY "Moderators can view all ride submissions"
|
||||||
|
ON public.ride_submissions
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- company_submissions
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update company submissions" ON public.company_submissions;
|
||||||
|
CREATE POLICY "Moderators can update company submissions"
|
||||||
|
ON public.company_submissions
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete company submissions" ON public.company_submissions;
|
||||||
|
CREATE POLICY "Moderators can delete company submissions"
|
||||||
|
ON public.company_submissions
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all company submissions" ON public.company_submissions;
|
||||||
|
CREATE POLICY "Moderators can view all company submissions"
|
||||||
|
ON public.company_submissions
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- photo_submissions
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update photo submissions" ON public.photo_submissions;
|
||||||
|
CREATE POLICY "Moderators can update photo submissions"
|
||||||
|
ON public.photo_submissions
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete photo submissions" ON public.photo_submissions;
|
||||||
|
CREATE POLICY "Moderators can delete photo submissions"
|
||||||
|
ON public.photo_submissions
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all photo submissions" ON public.photo_submissions;
|
||||||
|
CREATE POLICY "Moderators can view all photo submissions"
|
||||||
|
ON public.photo_submissions
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- photo_submission_items
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update photo submission items" ON public.photo_submission_items;
|
||||||
|
CREATE POLICY "Moderators can update photo submission items"
|
||||||
|
ON public.photo_submission_items
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete photo submission items" ON public.photo_submission_items;
|
||||||
|
CREATE POLICY "Moderators can delete photo submission items"
|
||||||
|
ON public.photo_submission_items
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all photo submission items" ON public.photo_submission_items;
|
||||||
|
CREATE POLICY "Moderators can view all photo submission items"
|
||||||
|
ON public.photo_submission_items
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 1.2 User Management Tables
|
||||||
|
|
||||||
|
-- profiles (moderator banning)
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update profiles for banning" ON public.profiles;
|
||||||
|
CREATE POLICY "Moderators can update profiles for banning"
|
||||||
|
ON public.profiles
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 1.3 Reports & Moderation
|
||||||
|
|
||||||
|
-- reports
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update reports" ON public.reports;
|
||||||
|
CREATE POLICY "Moderators can update reports"
|
||||||
|
ON public.reports
|
||||||
|
FOR UPDATE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete reports" ON public.reports;
|
||||||
|
CREATE POLICY "Moderators can delete reports"
|
||||||
|
ON public.reports
|
||||||
|
FOR DELETE
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can view all reports" ON public.reports;
|
||||||
|
CREATE POLICY "Moderators can view all reports"
|
||||||
|
ON public.reports
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 1.4 Blog Management
|
||||||
|
|
||||||
|
-- blog_posts (admin/superuser only)
|
||||||
|
DROP POLICY IF EXISTS "Admins and superusers can manage blog posts" ON public.blog_posts;
|
||||||
|
CREATE POLICY "Admins and superusers can manage blog posts"
|
||||||
|
ON public.blog_posts
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
(has_role(auth.uid(), 'admin'::app_role) OR has_role(auth.uid(), 'superuser'::app_role)) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 1.5 Admin Audit & Settings
|
||||||
|
|
||||||
|
-- admin_audit_log (already has AAL2, verify it's correct)
|
||||||
|
DROP POLICY IF EXISTS "Admins can insert audit log with MFA" ON public.admin_audit_log;
|
||||||
|
CREATE POLICY "Admins can insert audit log with MFA"
|
||||||
|
ON public.admin_audit_log
|
||||||
|
FOR INSERT
|
||||||
|
TO authenticated
|
||||||
|
WITH CHECK (
|
||||||
|
is_moderator(auth.uid()) AND has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Admins can view audit log" ON public.admin_audit_log;
|
||||||
|
CREATE POLICY "Admins can view audit log"
|
||||||
|
ON public.admin_audit_log
|
||||||
|
FOR SELECT
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid()) AND (
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT 1 FROM auth.mfa_factors
|
||||||
|
WHERE user_id = auth.uid() AND status = 'verified'
|
||||||
|
) OR has_aal2()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- admin_settings (superuser only)
|
||||||
|
DROP POLICY IF EXISTS "Superusers can manage settings with MFA" ON public.admin_settings;
|
||||||
|
CREATE POLICY "Superusers can manage settings with MFA"
|
||||||
|
ON public.admin_settings
|
||||||
|
FOR ALL
|
||||||
|
TO authenticated
|
||||||
|
USING (
|
||||||
|
is_superuser(auth.uid()) AND has_aal2()
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user