Security architecture upgrade + bug reporting system
Database & Security: - Dual password storage: bcrypt (auth) + pgcrypto AES-256 (recovery) - SUPER_ADMIN master key recovery system (master_keys table) - Row Level Security on customers, leads, opportunities, communications, tasks - SALES_USER: own records only - ADMIN: all records - SUPER_ADMIN: all records (bypasses RLS) - Immutable audit logs (DELETE/UPDATE blocked by triggers) - New audit event types: BUG_CREATED, BUG_UPDATED, BUG_ASSIGNED, BUG_RESOLVED, LOGIN, LOGOUT - Database export logging (database_export_logs table) - Backup logging with pg_dump script (scripts/backup.ps1) - Fixed audit constraint to allow new action types Authentication: - Random JWT secret generated on every dev server start (invalidates all prior sessions after restart) - Session cookie is now session-only (no maxAge) - setSessionContext() for RLS integration Bug Reporting System: - bug_reports table with RLS (insert by all, select/update by admin only) - POST /api/bug-reports (any authenticated user) - GET /api/bug-reports (admin/super_admin only) - PATCH /api/bug-reports/:id (admin/super_admin only) - POST /api/auth/recover (super_admin password recovery) - Audit logging for all bug report actions Other: - Added 'dev' to UserRole type - Bug report modal UI with severity selector - Added bug report button to topbar
This commit is contained in:
@@ -0,0 +1,604 @@
|
||||
-- ============================================================================
|
||||
-- CRM Security Architecture Upgrade
|
||||
-- Internal Company CRM — Prioritizes control, recovery, and maintainability
|
||||
-- ============================================================================
|
||||
-- Implements:
|
||||
-- • Dual password storage (bcrypt + pgcrypto AES reversible)
|
||||
-- • SUPER_ADMIN master key recovery system
|
||||
-- • Row Level Security (RLS) on CRM tables
|
||||
-- • Database export logging
|
||||
-- • Backup logging
|
||||
-- • Immutable admin action tracking
|
||||
-- • SQL injection defense (parameterized queries enforced app-side)
|
||||
-- ============================================================================
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. DUAL PASSWORD STORAGE
|
||||
-- ============================================================================
|
||||
-- password_hash (bcrypt) — used for normal login authentication
|
||||
-- password_encrypted (AES) — reversible encryption for emergency credential recovery
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS password_encrypted TEXT;
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. SUPER_ADMIN MASTER KEY SYSTEM
|
||||
-- ============================================================================
|
||||
-- Stores the master decryption key used with pgp_sym_encrypt/pgp_sym_decrypt.
|
||||
-- Only accessible by SUPER_ADMIN role via security definer functions.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS master_keys (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
key_name VARCHAR(100) NOT NULL UNIQUE,
|
||||
key_value TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_by UUID REFERENCES users(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_master_keys_active ON master_keys(key_name) WHERE is_active = TRUE;
|
||||
|
||||
-- ============================================================================
|
||||
-- 3. DATABASE EXPORT LOGGING
|
||||
-- ============================================================================
|
||||
-- Tracks all database exports and SQL dumps performed by SUPER_ADMIN.
|
||||
-- Immutable — never allow deletion or update.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS database_export_logs (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
exported_by UUID NOT NULL REFERENCES users(id),
|
||||
export_type VARCHAR(50) NOT NULL,
|
||||
file_name VARCHAR(500) NOT NULL,
|
||||
file_size_bytes BIGINT,
|
||||
record_count INT,
|
||||
ip_address INET,
|
||||
user_agent TEXT,
|
||||
notes TEXT,
|
||||
exported_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_export_logs_user ON database_export_logs(exported_by);
|
||||
CREATE INDEX idx_export_logs_time ON database_export_logs(exported_at DESC);
|
||||
|
||||
COMMENT ON TABLE database_export_logs IS 'Immutable audit of all database exports. Never DELETE or UPDATE rows.';
|
||||
COMMENT ON COLUMN database_export_logs.export_type IS 'pg_dump, csv_export, full_backup, selective_export';
|
||||
|
||||
-- ============================================================================
|
||||
-- 4. BACKUP LOGGING
|
||||
-- ============================================================================
|
||||
-- Records every automated or manual database backup.
|
||||
-- Retention: minimum 30 days of backup history.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backup_logs (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
backup_type VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
file_name VARCHAR(500),
|
||||
file_size_bytes BIGINT,
|
||||
pg_dump_exit_code INT,
|
||||
error_message TEXT,
|
||||
checksum VARCHAR(64),
|
||||
verification_status VARCHAR(20),
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ,
|
||||
duration_seconds INT,
|
||||
retention_days INT NOT NULL DEFAULT 30,
|
||||
triggered_by UUID REFERENCES users(id),
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX idx_backup_logs_time ON backup_logs(started_at DESC);
|
||||
CREATE INDEX idx_backup_logs_status ON backup_logs(status);
|
||||
CREATE INDEX idx_backup_logs_type ON backup_logs(backup_type);
|
||||
CREATE INDEX idx_backup_logs_completed ON backup_logs(status)
|
||||
WHERE status = 'completed';
|
||||
|
||||
-- ============================================================================
|
||||
-- 5. AUDIT TRIGGER: Password changes
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit_password_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF OLD.password_hash IS DISTINCT FROM NEW.password_hash THEN
|
||||
INSERT INTO audit_logs (
|
||||
table_name, record_id, action, old_data, new_data, changed_by, ip_address
|
||||
) VALUES (
|
||||
'users',
|
||||
NEW.id,
|
||||
'UPDATE',
|
||||
jsonb_build_object('password_changed', true, 'password_change_required', OLD.password_change_required),
|
||||
jsonb_build_object('password_changed', true, 'password_change_required', NEW.password_change_required),
|
||||
NULLIF(current_setting('app.current_user_id', true), ''),
|
||||
NULLIF(current_setting('app.current_ip', true), '')
|
||||
);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_audit_password_change ON users;
|
||||
CREATE TRIGGER trg_audit_password_change
|
||||
AFTER UPDATE OF password_hash ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION audit_password_change();
|
||||
|
||||
-- ============================================================================
|
||||
-- 6. AUDIT TRIGGER: User creation
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit_user_creation()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO audit_logs (
|
||||
table_name, record_id, action, new_data, changed_by
|
||||
) VALUES (
|
||||
'users',
|
||||
NEW.id,
|
||||
'CREATE',
|
||||
jsonb_build_object(
|
||||
'username', NEW.username,
|
||||
'email', NEW.email,
|
||||
'first_name', NEW.first_name,
|
||||
'last_name', NEW.last_name,
|
||||
'is_active', NEW.is_active
|
||||
),
|
||||
NEW.created_by
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_audit_user_create ON users;
|
||||
CREATE TRIGGER trg_audit_user_create
|
||||
AFTER INSERT ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION audit_user_creation();
|
||||
|
||||
-- ============================================================================
|
||||
-- 7. AUDIT TRIGGER: Database exports
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION audit_database_export()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO audit_logs (
|
||||
table_name, record_id, action, new_data, changed_by
|
||||
) VALUES (
|
||||
'database_export_logs',
|
||||
NEW.id,
|
||||
'CREATE',
|
||||
jsonb_build_object(
|
||||
'export_type', NEW.export_type,
|
||||
'file_name', NEW.file_name,
|
||||
'file_size_bytes', NEW.file_size_bytes,
|
||||
'record_count', NEW.record_count
|
||||
),
|
||||
NEW.exported_by
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_audit_database_export ON database_export_logs;
|
||||
CREATE TRIGGER trg_audit_database_export
|
||||
AFTER INSERT ON database_export_logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION audit_database_export();
|
||||
|
||||
-- ============================================================================
|
||||
-- 8. AUDIT TRIGGER: Login/logout already exists via login_attempts trigger
|
||||
-- (trg_audit_login in 001_schema.sql)
|
||||
-- This enhances it to also track session-level events.
|
||||
-- ============================================================================
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_audit_login ON login_attempts;
|
||||
CREATE TRIGGER trg_audit_login
|
||||
AFTER INSERT ON login_attempts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION audit_login();
|
||||
|
||||
-- ============================================================================
|
||||
-- 9. ROW LEVEL SECURITY
|
||||
-- ============================================================================
|
||||
-- RLS policies enforce data visibility per role:
|
||||
-- SALES_USER → only sees records assigned to them
|
||||
-- ADMIN → sees operational records for investigation
|
||||
-- SUPER_ADMIN → sees everything
|
||||
-- ============================================================================
|
||||
|
||||
-- Helper function to get current user's role hierarchy level
|
||||
CREATE OR REPLACE FUNCTION current_user_hierarchy_level()
|
||||
RETURNS INT AS $$
|
||||
DECLARE
|
||||
uid UUID;
|
||||
BEGIN
|
||||
BEGIN
|
||||
uid := NULLIF(current_setting('app.current_user_id', true), '')::UUID;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN NULL;
|
||||
END;
|
||||
|
||||
IF uid IS NULL THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
RETURN (
|
||||
SELECT MIN(r.hierarchy_level)
|
||||
FROM user_roles ur
|
||||
JOIN roles r ON r.id = ur.role_id
|
||||
WHERE ur.user_id = uid
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- Helper function to get current user's ID from session setting
|
||||
CREATE OR REPLACE FUNCTION current_user_id()
|
||||
RETURNS UUID AS $$
|
||||
BEGIN
|
||||
BEGIN
|
||||
RETURN NULLIF(current_setting('app.current_user_id', true), '')::UUID;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
RETURN NULL;
|
||||
END;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE;
|
||||
|
||||
-- SALES_USER level = 3, ADMIN level = 2, SUPER_ADMIN level = 1
|
||||
|
||||
-- ── customers ──
|
||||
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS rls_customers_select ON customers;
|
||||
CREATE POLICY rls_customers_select ON customers
|
||||
FOR SELECT
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NULL
|
||||
OR current_user_hierarchy_level() <= 2 -- ADMIN and SUPER_ADMIN see all
|
||||
OR owner_id = current_user_id()
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_customers_insert ON customers;
|
||||
CREATE POLICY rls_customers_insert ON customers
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND (
|
||||
current_user_hierarchy_level() <= 2 -- ADMIN and SUPER_ADMIN can assign any owner
|
||||
OR owner_id = current_user_id() -- SALES_USER can only assign to self
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_customers_update ON customers;
|
||||
CREATE POLICY rls_customers_update ON customers
|
||||
FOR UPDATE
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND (
|
||||
current_user_hierarchy_level() <= 2
|
||||
OR owner_id = current_user_id()
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND (
|
||||
current_user_hierarchy_level() <= 2
|
||||
OR (
|
||||
owner_id = current_user_id()
|
||||
AND owner_id = current_user_id() -- SALES_USER cannot reassign ownership
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_customers_delete ON customers;
|
||||
CREATE POLICY rls_customers_delete ON customers
|
||||
FOR DELETE
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND current_user_hierarchy_level() <= 2
|
||||
);
|
||||
|
||||
-- ── leads ──
|
||||
ALTER TABLE leads ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS rls_leads_select ON leads;
|
||||
CREATE POLICY rls_leads_select ON leads
|
||||
FOR SELECT
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NULL
|
||||
OR current_user_hierarchy_level() <= 2
|
||||
OR assigned_to = current_user_id()
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_leads_insert ON leads;
|
||||
CREATE POLICY rls_leads_insert ON leads
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND (
|
||||
current_user_hierarchy_level() <= 2
|
||||
OR assigned_to = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_leads_update ON leads;
|
||||
CREATE POLICY rls_leads_update ON leads
|
||||
FOR UPDATE
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND (
|
||||
current_user_hierarchy_level() <= 2
|
||||
OR assigned_to = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_leads_delete ON leads;
|
||||
CREATE POLICY rls_leads_delete ON leads
|
||||
FOR DELETE
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND current_user_hierarchy_level() <= 2
|
||||
);
|
||||
|
||||
-- ── opportunities ──
|
||||
ALTER TABLE opportunities ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS rls_opportunities_select ON opportunities;
|
||||
CREATE POLICY rls_opportunities_select ON opportunities
|
||||
FOR SELECT
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NULL
|
||||
OR current_user_hierarchy_level() <= 2
|
||||
OR owner_id = current_user_id()
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_opportunities_insert ON opportunities;
|
||||
CREATE POLICY rls_opportunities_insert ON opportunities
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND (
|
||||
current_user_hierarchy_level() <= 2
|
||||
OR owner_id = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_opportunities_update ON opportunities;
|
||||
CREATE POLICY rls_opportunities_update ON opportunities
|
||||
FOR UPDATE
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND (
|
||||
current_user_hierarchy_level() <= 2
|
||||
OR owner_id = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_opportunities_delete ON opportunities;
|
||||
CREATE POLICY rls_opportunities_delete ON opportunities
|
||||
FOR DELETE
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND current_user_hierarchy_level() <= 2
|
||||
);
|
||||
|
||||
-- ── communications ──
|
||||
ALTER TABLE communications ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS rls_communications_select ON communications;
|
||||
CREATE POLICY rls_communications_select ON communications
|
||||
FOR SELECT
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NULL
|
||||
OR current_user_hierarchy_level() <= 2
|
||||
OR created_by = current_user_id()
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_communications_insert ON communications;
|
||||
CREATE POLICY rls_communications_insert ON communications
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_communications_delete ON communications;
|
||||
CREATE POLICY rls_communications_delete ON communications
|
||||
FOR DELETE
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND current_user_hierarchy_level() <= 2
|
||||
);
|
||||
|
||||
-- ── tasks ──
|
||||
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP POLICY IF EXISTS rls_tasks_select ON tasks;
|
||||
CREATE POLICY rls_tasks_select ON tasks
|
||||
FOR SELECT
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NULL
|
||||
OR current_user_hierarchy_level() <= 2
|
||||
OR assigned_to = current_user_id()
|
||||
OR assigned_by = current_user_id()
|
||||
);
|
||||
|
||||
DROP POLICY IF EXISTS rls_tasks_update ON tasks;
|
||||
CREATE POLICY rls_tasks_update ON tasks
|
||||
FOR UPDATE
|
||||
USING (
|
||||
current_user_hierarchy_level() IS NOT NULL
|
||||
AND (
|
||||
current_user_hierarchy_level() <= 2
|
||||
OR assigned_to = current_user_id()
|
||||
OR assigned_by = current_user_id()
|
||||
)
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- 10. IMMUTABLE AUDIT LOG PROTECTION
|
||||
-- ============================================================================
|
||||
-- Prevent deletion or update of audit_logs at the database level.
|
||||
-- Even SUPER_ADMIN cannot delete historical audit records.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION prevent_audit_mutation()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'IMMUTABLE: audit_logs cannot be modified or deleted. All changes are permanently recorded.';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_prevent_audit_delete ON audit_logs;
|
||||
CREATE TRIGGER trg_prevent_audit_delete
|
||||
BEFORE DELETE ON audit_logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION prevent_audit_mutation();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_prevent_audit_update ON audit_logs;
|
||||
CREATE TRIGGER trg_prevent_audit_update
|
||||
BEFORE UPDATE ON audit_logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION prevent_audit_mutation();
|
||||
|
||||
-- Same protection for database_export_logs
|
||||
DROP TRIGGER IF EXISTS trg_prevent_export_delete ON database_export_logs;
|
||||
CREATE TRIGGER trg_prevent_export_delete
|
||||
BEFORE DELETE ON database_export_logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION prevent_audit_mutation();
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_prevent_export_update ON database_export_logs;
|
||||
CREATE TRIGGER trg_prevent_export_update
|
||||
BEFORE UPDATE ON database_export_logs
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION prevent_audit_mutation();
|
||||
|
||||
-- ============================================================================
|
||||
-- 11. ENCRYPTION FUNCTIONS
|
||||
-- ============================================================================
|
||||
-- Uses pgcrypto's pgp_sym_encrypt / pgp_sym_decrypt with the master key.
|
||||
-- Master key is stored in master_keys table, fetched by security definer functions.
|
||||
-- ============================================================================
|
||||
|
||||
-- Get the active master decryption key
|
||||
-- Only callable by SUPER_ADMIN
|
||||
CREATE OR REPLACE FUNCTION get_master_decryption_key()
|
||||
RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
key_value TEXT;
|
||||
uid UUID;
|
||||
user_level INT;
|
||||
BEGIN
|
||||
uid := current_user_id();
|
||||
IF uid IS NULL THEN
|
||||
RAISE EXCEPTION 'Not authenticated';
|
||||
END IF;
|
||||
|
||||
user_level := current_user_hierarchy_level();
|
||||
IF user_level IS NULL OR user_level > 1 THEN
|
||||
RAISE EXCEPTION 'ACCESS DENIED: Only SUPER_ADMIN can access the master decryption key';
|
||||
END IF;
|
||||
|
||||
SELECT mk.key_value INTO key_value
|
||||
FROM master_keys mk
|
||||
WHERE mk.is_active = TRUE
|
||||
ORDER BY mk.created_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
RETURN key_value;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Encrypt a plaintext password using the active master key
|
||||
CREATE OR REPLACE FUNCTION encrypt_password(p_plaintext TEXT)
|
||||
RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
master_key TEXT;
|
||||
BEGIN
|
||||
SELECT key_value INTO master_key
|
||||
FROM master_keys
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
IF master_key IS NULL THEN
|
||||
RAISE EXCEPTION 'No active master key found. Contact SUPER_ADMIN.';
|
||||
END IF;
|
||||
|
||||
RETURN encode(
|
||||
pgp_sym_encrypt(p_plaintext, master_key, 'compress-algo=2, cipher-algo=aes256'),
|
||||
'escape'
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Decrypt a password that was encrypted with encrypt_password()
|
||||
CREATE OR REPLACE FUNCTION decrypt_password(p_encrypted TEXT)
|
||||
RETURNS TEXT AS $$
|
||||
DECLARE
|
||||
master_key TEXT;
|
||||
BEGIN
|
||||
master_key := get_master_decryption_key();
|
||||
RETURN pgp_sym_decrypt(decode(p_encrypted, 'escape'), master_key);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- ============================================================================
|
||||
-- 12. RLS BYPASS FOR SUPER_ADMIN
|
||||
-- ============================================================================
|
||||
-- SUPER_ADMIN bypasses all RLS. This function is used by triggers/policies
|
||||
-- to check if the current user should bypass restrictions.
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION is_super_admin()
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN COALESCE(current_user_hierarchy_level(), 999) <= 1;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql STABLE SECURITY DEFINER;
|
||||
|
||||
-- ============================================================================
|
||||
-- 13. SEED DATA: Master key (for development/testing)
|
||||
-- ============================================================================
|
||||
-- In production, this key should be set via a secure deployment process.
|
||||
-- The key is stored in the database and encrypted at rest by PostgreSQL.
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO master_keys (key_name, key_value, description, created_by)
|
||||
SELECT
|
||||
'MASTER_DECRYPTION_KEY',
|
||||
encode(gen_random_bytes(32), 'hex'),
|
||||
'Master key for reversible password encryption. Used with pgp_sym_encrypt/decrypt.',
|
||||
'00000000-0000-0000-0000-000000000001'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM master_keys WHERE key_name = 'MASTER_DECRYPTION_KEY'
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- 14. UPDATE SEED DATA: Encrypt passwords for existing test accounts
|
||||
-- ============================================================================
|
||||
|
||||
UPDATE users SET password_encrypted = encrypt_password('SuperAdmin@2026')
|
||||
WHERE id = '00000000-0000-0000-0000-000000000001' AND password_encrypted IS NULL;
|
||||
|
||||
UPDATE users SET password_encrypted = encrypt_password('AdminAccess@2026')
|
||||
WHERE id = '00000000-0000-0000-0000-000000000002' AND password_encrypted IS NULL;
|
||||
|
||||
UPDATE users SET password_encrypted = encrypt_password('SalesAccess@2026')
|
||||
WHERE id = '00000000-0000-0000-0000-000000000003' AND password_encrypted IS NULL;
|
||||
|
||||
UPDATE users SET password_encrypted = encrypt_password('DevTesting@2026')
|
||||
WHERE id = '00000000-0000-0000-0000-000000000004' AND password_encrypted IS NULL;
|
||||
|
||||
-- ============================================================================
|
||||
-- FUTURE REQUIREMENT NOTE:
|
||||
-- If this system is ever exposed publicly, remove reversible password storage
|
||||
-- immediately. Keep bcrypt only. Delete the password_encrypted column and
|
||||
-- master_keys table. The MASTER_DECRYPTION_KEY must never be exposed externally.
|
||||
-- ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user