mirror of
https://git.coastit.co.za/caitlin/CRM_ENVR.git
synced 2026-07-10 19:17:16 +02:00
605 lines
21 KiB
PL/PgSQL
605 lines
21 KiB
PL/PgSQL
-- ============================================================================
|
|
-- 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.
|
|
-- ============================================================================
|
|
|