mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -05:00
Implement MFA Enforcement
This commit is contained in:
24
src/components/auth/MFARequiredAlert.tsx
Normal file
24
src/components/auth/MFARequiredAlert.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Shield } from 'lucide-react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export function MFARequiredAlert() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert variant="destructive" className="my-4">
|
||||||
|
<Shield className="h-4 w-4" />
|
||||||
|
<AlertTitle>Two-Factor Authentication Required</AlertTitle>
|
||||||
|
<AlertDescription className="mt-2 space-y-3">
|
||||||
|
<p>Your role requires two-factor authentication to access this area.</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate('/settings?tab=security')}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Set up MFA
|
||||||
|
</Button>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -113,16 +113,14 @@ export function TOTPSetup() {
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: 'TOTP Enabled',
|
title: 'TOTP Enabled',
|
||||||
description: 'Two-factor authentication has been successfully enabled for your account.'
|
description: 'Please sign in again to activate MFA protection.'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset state and refresh factors
|
// Force sign out to get new session with AAL2
|
||||||
setEnrolling(false);
|
setTimeout(async () => {
|
||||||
setQrCode('');
|
await supabase.auth.signOut();
|
||||||
setSecret('');
|
window.location.href = '/auth';
|
||||||
setFactorId('');
|
}, 2000);
|
||||||
setVerificationCode('');
|
|
||||||
fetchTOTPFactors();
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { authLog, authWarn, authError } from '@/lib/authLogger';
|
|||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
|
aal: 'aal1' | 'aal2' | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
pendingEmail: string | null;
|
pendingEmail: string | null;
|
||||||
sessionError: string | null;
|
sessionError: string | null;
|
||||||
@@ -21,6 +22,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
|
const [aal, setAal] = useState<'aal1' | 'aal2' | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
|
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
|
||||||
const [sessionError, setSessionError] = useState<string | null>(null);
|
const [sessionError, setSessionError] = useState<string | null>(null);
|
||||||
@@ -84,17 +86,22 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
|||||||
authLog('[Auth] SIGNED_IN - user authenticated');
|
authLog('[Auth] SIGNED_IN - user authenticated');
|
||||||
setSession(session);
|
setSession(session);
|
||||||
setUser(session.user);
|
setUser(session.user);
|
||||||
|
const userAal = (session.user as any).aal as 'aal1' | 'aal2' | undefined;
|
||||||
|
setAal(userAal || 'aal1');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else if (event === 'INITIAL_SESSION') {
|
} else if (event === 'INITIAL_SESSION') {
|
||||||
if (session?.user) {
|
if (session?.user) {
|
||||||
authLog('[Auth] INITIAL_SESSION - user exists');
|
authLog('[Auth] INITIAL_SESSION - user exists');
|
||||||
setSession(session);
|
setSession(session);
|
||||||
setUser(session.user);
|
setUser(session.user);
|
||||||
|
const userAal = (session.user as any).aal as 'aal1' | 'aal2' | undefined;
|
||||||
|
setAal(userAal || 'aal1');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
authLog('[Auth] INITIAL_SESSION - no user');
|
authLog('[Auth] INITIAL_SESSION - no user');
|
||||||
setSession(null);
|
setSession(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
setAal(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -102,11 +109,14 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
|||||||
authLog('[Auth] SIGNED_OUT - clearing state');
|
authLog('[Auth] SIGNED_OUT - clearing state');
|
||||||
setSession(null);
|
setSession(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
|
setAal(null);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
setSession(session);
|
setSession(session);
|
||||||
setUser(session?.user ?? null);
|
setUser(session?.user ?? null);
|
||||||
|
const userAal = session?.user ? ((session.user as any).aal as 'aal1' | 'aal2' | undefined) : null;
|
||||||
|
setAal(userAal || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect confirmed email change: email changed AND no longer pending
|
// Detect confirmed email change: email changed AND no longer pending
|
||||||
@@ -214,6 +224,7 @@ function AuthProviderComponent({ children }: { children: React.ReactNode }) {
|
|||||||
const value = {
|
const value = {
|
||||||
user,
|
user,
|
||||||
session,
|
session,
|
||||||
|
aal,
|
||||||
loading,
|
loading,
|
||||||
pendingEmail,
|
pendingEmail,
|
||||||
sessionError,
|
sessionError,
|
||||||
|
|||||||
21
src/hooks/useRequireMFA.ts
Normal file
21
src/hooks/useRequireMFA.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useAuth } from './useAuth';
|
||||||
|
import { useUserRole } from './useUserRole';
|
||||||
|
|
||||||
|
export function useRequireMFA() {
|
||||||
|
const { aal } = useAuth();
|
||||||
|
const { isModerator, isAdmin, loading } = useUserRole();
|
||||||
|
|
||||||
|
// MFA is required for moderators and admins
|
||||||
|
const requiresMFA = isModerator() || isAdmin();
|
||||||
|
|
||||||
|
// User has MFA if they have AAL2
|
||||||
|
const hasMFA = aal === 'aal2';
|
||||||
|
|
||||||
|
return {
|
||||||
|
requiresMFA,
|
||||||
|
hasMFA,
|
||||||
|
needsEnrollment: requiresMFA && !hasMFA,
|
||||||
|
aal,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3012,6 +3012,10 @@ export type Database = {
|
|||||||
Args: { _user_id: string }
|
Args: { _user_id: string }
|
||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
|
has_aal2: {
|
||||||
|
Args: Record<PropertyKey, never>
|
||||||
|
Returns: boolean
|
||||||
|
}
|
||||||
has_pending_dependents: {
|
has_pending_dependents: {
|
||||||
Args: { item_id: string }
|
Args: { item_id: string }
|
||||||
Returns: boolean
|
Returns: boolean
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { FileText, Flag, AlertCircle, Activity, ShieldAlert } from 'lucide-react';
|
import { FileText, Flag, AlertCircle, Activity, ShieldAlert } from 'lucide-react';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { useRequireMFA } from '@/hooks/useRequireMFA';
|
||||||
|
import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||||
@@ -20,6 +22,7 @@ import { QueueSkeleton } from '@/components/moderation/QueueSkeleton';
|
|||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
const { user, loading: authLoading } = useAuth();
|
const { user, loading: authLoading } = useAuth();
|
||||||
const { isModerator, loading: roleLoading } = useUserRole();
|
const { isModerator, loading: roleLoading } = useUserRole();
|
||||||
|
const { needsEnrollment, loading: mfaLoading } = useRequireMFA();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState('moderation');
|
const [activeTab, setActiveTab] = useState('moderation');
|
||||||
@@ -110,7 +113,7 @@ export default function AdminDashboard() {
|
|||||||
}
|
}
|
||||||
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
||||||
|
|
||||||
if (authLoading || roleLoading) {
|
if (authLoading || roleLoading || mfaLoading) {
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -150,6 +153,15 @@ export default function AdminDashboard() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MFA enforcement
|
||||||
|
if (needsEnrollment) {
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<MFARequiredAlert />
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const statCards = [
|
const statCards = [
|
||||||
{
|
{
|
||||||
label: 'Pending Submissions',
|
label: 'Pending Submissions',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useRef, useEffect, useCallback } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { useRequireMFA } from '@/hooks/useRequireMFA';
|
||||||
|
import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert';
|
||||||
import { AdminLayout } from '@/components/layout/AdminLayout';
|
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||||
import { ModerationQueue, ModerationQueueRef } from '@/components/moderation/ModerationQueue';
|
import { ModerationQueue, ModerationQueueRef } from '@/components/moderation/ModerationQueue';
|
||||||
import { QueueSkeleton } from '@/components/moderation/QueueSkeleton';
|
import { QueueSkeleton } from '@/components/moderation/QueueSkeleton';
|
||||||
@@ -11,6 +13,7 @@ import { useModerationStats } from '@/hooks/useModerationStats';
|
|||||||
export default function AdminModeration() {
|
export default function AdminModeration() {
|
||||||
const { user, loading: authLoading } = useAuth();
|
const { user, loading: authLoading } = useAuth();
|
||||||
const { isModerator, loading: roleLoading } = useUserRole();
|
const { isModerator, loading: roleLoading } = useUserRole();
|
||||||
|
const { needsEnrollment, loading: mfaLoading } = useRequireMFA();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const moderationQueueRef = useRef<ModerationQueueRef>(null);
|
const moderationQueueRef = useRef<ModerationQueueRef>(null);
|
||||||
|
|
||||||
@@ -46,7 +49,7 @@ export default function AdminModeration() {
|
|||||||
}
|
}
|
||||||
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
||||||
|
|
||||||
if (authLoading || roleLoading) {
|
if (authLoading || roleLoading || mfaLoading) {
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AdminLayout
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
@@ -72,6 +75,15 @@ export default function AdminModeration() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MFA enforcement
|
||||||
|
if (needsEnrollment) {
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<MFARequiredAlert />
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AdminLayout
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useRef, useEffect, useCallback } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { useRequireMFA } from '@/hooks/useRequireMFA';
|
||||||
|
import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert';
|
||||||
import { AdminLayout } from '@/components/layout/AdminLayout';
|
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||||
import { ReportsQueue, ReportsQueueRef } from '@/components/moderation/ReportsQueue';
|
import { ReportsQueue, ReportsQueueRef } from '@/components/moderation/ReportsQueue';
|
||||||
import { QueueSkeleton } from '@/components/moderation/QueueSkeleton';
|
import { QueueSkeleton } from '@/components/moderation/QueueSkeleton';
|
||||||
@@ -11,6 +13,7 @@ import { useModerationStats } from '@/hooks/useModerationStats';
|
|||||||
export default function AdminReports() {
|
export default function AdminReports() {
|
||||||
const { user, loading: authLoading } = useAuth();
|
const { user, loading: authLoading } = useAuth();
|
||||||
const { isModerator, loading: roleLoading } = useUserRole();
|
const { isModerator, loading: roleLoading } = useUserRole();
|
||||||
|
const { needsEnrollment, loading: mfaLoading } = useRequireMFA();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const reportsQueueRef = useRef<ReportsQueueRef>(null);
|
const reportsQueueRef = useRef<ReportsQueueRef>(null);
|
||||||
|
|
||||||
@@ -47,7 +50,7 @@ export default function AdminReports() {
|
|||||||
}
|
}
|
||||||
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
||||||
|
|
||||||
if (authLoading || roleLoading) {
|
if (authLoading || roleLoading || mfaLoading) {
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AdminLayout
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
@@ -73,6 +76,15 @@ export default function AdminReports() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MFA enforcement
|
||||||
|
if (needsEnrollment) {
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<MFARequiredAlert />
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout
|
<AdminLayout
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useUserRole } from '@/hooks/useUserRole';
|
import { useUserRole } from '@/hooks/useUserRole';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
import { useRequireMFA } from '@/hooks/useRequireMFA';
|
||||||
|
import { MFARequiredAlert } from '@/components/auth/MFARequiredAlert';
|
||||||
import { AdminLayout } from '@/components/layout/AdminLayout';
|
import { AdminLayout } from '@/components/layout/AdminLayout';
|
||||||
import { UserManagement } from '@/components/admin/UserManagement';
|
import { UserManagement } from '@/components/admin/UserManagement';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
@@ -10,6 +12,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
export default function AdminUsers() {
|
export default function AdminUsers() {
|
||||||
const { user, loading: authLoading } = useAuth();
|
const { user, loading: authLoading } = useAuth();
|
||||||
const { isModerator, loading: roleLoading } = useUserRole();
|
const { isModerator, loading: roleLoading } = useUserRole();
|
||||||
|
const { needsEnrollment, loading: mfaLoading } = useRequireMFA();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -26,7 +29,7 @@ export default function AdminUsers() {
|
|||||||
}
|
}
|
||||||
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
}, [user, authLoading, roleLoading, navigate, isModerator]);
|
||||||
|
|
||||||
if (authLoading || roleLoading) {
|
if (authLoading || roleLoading || mfaLoading) {
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -60,6 +63,15 @@ export default function AdminUsers() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MFA enforcement
|
||||||
|
if (needsEnrollment) {
|
||||||
|
return (
|
||||||
|
<AdminLayout>
|
||||||
|
<MFARequiredAlert />
|
||||||
|
</AdminLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
-- Create helper function to check AAL2 (Authenticator Assurance Level 2)
|
||||||
|
CREATE OR REPLACE FUNCTION public.has_aal2()
|
||||||
|
RETURNS boolean
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
SELECT COALESCE((auth.jwt()->>'aal')::text = 'aal2', false);
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Update admin_settings policies to require MFA
|
||||||
|
DROP POLICY IF EXISTS "Superusers can manage settings" ON public.admin_settings;
|
||||||
|
CREATE POLICY "Superusers can manage settings with MFA"
|
||||||
|
ON public.admin_settings
|
||||||
|
FOR ALL
|
||||||
|
USING (
|
||||||
|
is_superuser(auth.uid())
|
||||||
|
AND public.has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update user_roles policies to require MFA for role management
|
||||||
|
DROP POLICY IF EXISTS "Admins can insert user roles" ON public.user_roles;
|
||||||
|
CREATE POLICY "Admins can insert user roles with MFA"
|
||||||
|
ON public.user_roles
|
||||||
|
FOR INSERT
|
||||||
|
WITH CHECK (
|
||||||
|
(has_role(auth.uid(), 'admin'::app_role) OR is_superuser(auth.uid()))
|
||||||
|
AND public.has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Admins can delete user roles" ON public.user_roles;
|
||||||
|
CREATE POLICY "Admins can delete user roles with MFA"
|
||||||
|
ON public.user_roles
|
||||||
|
FOR DELETE
|
||||||
|
USING (
|
||||||
|
(has_role(auth.uid(), 'admin'::app_role) OR is_superuser(auth.uid()))
|
||||||
|
AND public.has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update content_submissions moderation policies to require MFA
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update content submissions" ON public.content_submissions;
|
||||||
|
CREATE POLICY "Moderators can update submissions with MFA"
|
||||||
|
ON public.content_submissions
|
||||||
|
FOR UPDATE
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid())
|
||||||
|
AND public.has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
DROP POLICY IF EXISTS "Moderators can delete content submissions" ON public.content_submissions;
|
||||||
|
CREATE POLICY "Moderators can delete submissions with MFA"
|
||||||
|
ON public.content_submissions
|
||||||
|
FOR DELETE
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid())
|
||||||
|
AND public.has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update submission_items policies to require MFA
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update submission items" ON public.submission_items;
|
||||||
|
CREATE POLICY "Moderators can update submission items with MFA"
|
||||||
|
ON public.submission_items
|
||||||
|
FOR UPDATE
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid())
|
||||||
|
AND public.has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update reports policies to require MFA
|
||||||
|
DROP POLICY IF EXISTS "Moderators can update reports" ON public.reports;
|
||||||
|
CREATE POLICY "Moderators can update reports with MFA"
|
||||||
|
ON public.reports
|
||||||
|
FOR UPDATE
|
||||||
|
USING (
|
||||||
|
is_moderator(auth.uid())
|
||||||
|
AND public.has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update admin_audit_log policies to require MFA
|
||||||
|
DROP POLICY IF EXISTS "Admins can insert audit log" ON public.admin_audit_log;
|
||||||
|
CREATE POLICY "Admins can insert audit log with MFA"
|
||||||
|
ON public.admin_audit_log
|
||||||
|
FOR INSERT
|
||||||
|
WITH CHECK (
|
||||||
|
is_moderator(auth.uid())
|
||||||
|
AND public.has_aal2()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Update profiles policies for sensitive operations
|
||||||
|
DROP POLICY IF EXISTS "Admins can update any profile" ON public.profiles;
|
||||||
|
CREATE POLICY "Admins can update any profile with MFA"
|
||||||
|
ON public.profiles
|
||||||
|
FOR UPDATE
|
||||||
|
USING (
|
||||||
|
(auth.uid() = user_id) OR
|
||||||
|
((has_role(auth.uid(), 'admin'::app_role) OR is_superuser(auth.uid())) AND public.has_aal2())
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user