173 lines
5.2 KiB
PL/PgSQL
173 lines
5.2 KiB
PL/PgSQL
-- =============================================================================
|
|
-- AUDIT LOG
|
|
-- =============================================================================
|
|
-- Optional audit logging for tracking changes to security-critical tables
|
|
-- Enable/disable per table with triggers
|
|
|
|
CREATE TABLE morbac.audit_log (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
user_id UUID,
|
|
org_id UUID,
|
|
table_name TEXT NOT NULL,
|
|
operation TEXT NOT NULL,
|
|
record_id UUID,
|
|
old_data JSONB,
|
|
new_data JSONB,
|
|
changed_fields TEXT[],
|
|
session_user TEXT DEFAULT SESSION_USER,
|
|
client_addr INET DEFAULT INET_CLIENT_ADDR(),
|
|
application_name TEXT DEFAULT CURRENT_SETTING('application_name', true),
|
|
metadata JSONB DEFAULT '{}'::jsonb
|
|
);
|
|
|
|
CREATE INDEX idx_audit_log_timestamp ON morbac.audit_log(timestamp DESC);
|
|
CREATE INDEX idx_audit_log_user_id ON morbac.audit_log(user_id);
|
|
CREATE INDEX idx_audit_log_org_id ON morbac.audit_log(org_id);
|
|
CREATE INDEX idx_audit_log_table_operation ON morbac.audit_log(table_name, operation);
|
|
CREATE INDEX idx_audit_log_record_id ON morbac.audit_log(record_id);
|
|
|
|
COMMENT ON TABLE morbac.audit_log IS 'Audit trail for security-critical operations';
|
|
COMMENT ON COLUMN morbac.audit_log.user_id IS 'Application user (from morbac.current_user_id() if available)';
|
|
COMMENT ON COLUMN morbac.audit_log.org_id IS 'Organization context (from morbac.current_org_id() if available)';
|
|
COMMENT ON COLUMN morbac.audit_log.operation IS 'INSERT, UPDATE, DELETE, or custom operation name';
|
|
COMMENT ON COLUMN morbac.audit_log.changed_fields IS 'Array of field names that changed (for UPDATE operations)';
|
|
COMMENT ON COLUMN morbac.audit_log.session_user IS 'Database session user';
|
|
COMMENT ON COLUMN morbac.audit_log.client_addr IS 'Client IP address';
|
|
|
|
-- Generic audit trigger function
|
|
CREATE OR REPLACE FUNCTION morbac.audit_trigger()
|
|
RETURNS TRIGGER
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_old_data JSONB;
|
|
v_new_data JSONB;
|
|
v_changed_fields TEXT[];
|
|
v_user_id UUID;
|
|
v_org_id UUID;
|
|
v_record_id UUID;
|
|
BEGIN
|
|
-- Try to get current user/org context
|
|
BEGIN
|
|
v_user_id := morbac.current_user_id();
|
|
EXCEPTION WHEN OTHERS THEN
|
|
v_user_id := NULL;
|
|
END;
|
|
|
|
BEGIN
|
|
v_org_id := morbac.current_org_id();
|
|
EXCEPTION WHEN OTHERS THEN
|
|
v_org_id := NULL;
|
|
END;
|
|
|
|
-- Handle different operations
|
|
IF TG_OP = 'DELETE' THEN
|
|
v_old_data := row_to_json(OLD)::jsonb;
|
|
v_new_data := NULL;
|
|
v_record_id := (v_old_data->>'id')::uuid;
|
|
ELSIF TG_OP = 'INSERT' THEN
|
|
v_old_data := NULL;
|
|
v_new_data := row_to_json(NEW)::jsonb;
|
|
v_record_id := (v_new_data->>'id')::uuid;
|
|
ELSIF TG_OP = 'UPDATE' THEN
|
|
v_old_data := row_to_json(OLD)::jsonb;
|
|
v_new_data := row_to_json(NEW)::jsonb;
|
|
v_record_id := (v_new_data->>'id')::uuid;
|
|
|
|
-- Identify changed fields
|
|
SELECT array_agg(key)
|
|
INTO v_changed_fields
|
|
FROM jsonb_each(v_old_data)
|
|
WHERE v_old_data->key IS DISTINCT FROM v_new_data->key;
|
|
END IF;
|
|
|
|
-- Override org_id from record if available
|
|
IF v_org_id IS NULL THEN
|
|
IF v_new_data ? 'org_id' THEN
|
|
v_org_id := (v_new_data->>'org_id')::uuid;
|
|
ELSIF v_old_data ? 'org_id' THEN
|
|
v_org_id := (v_old_data->>'org_id')::uuid;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Insert audit record
|
|
INSERT INTO morbac.audit_log (
|
|
user_id,
|
|
org_id,
|
|
table_name,
|
|
operation,
|
|
record_id,
|
|
old_data,
|
|
new_data,
|
|
changed_fields
|
|
) VALUES (
|
|
v_user_id,
|
|
v_org_id,
|
|
TG_TABLE_NAME,
|
|
TG_OP,
|
|
v_record_id,
|
|
v_old_data,
|
|
v_new_data,
|
|
v_changed_fields
|
|
);
|
|
|
|
-- Return appropriate value
|
|
IF TG_OP = 'DELETE' THEN
|
|
RETURN OLD;
|
|
ELSE
|
|
RETURN NEW;
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION morbac.audit_trigger() IS
|
|
'Generic audit trigger function - captures INSERT/UPDATE/DELETE operations';
|
|
|
|
-- Helper function to enable audit logging on a table
|
|
CREATE OR REPLACE FUNCTION morbac.enable_audit(p_table_name TEXT)
|
|
RETURNS VOID
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_trigger_name TEXT;
|
|
BEGIN
|
|
v_trigger_name := 'audit_' || p_table_name;
|
|
|
|
EXECUTE format(
|
|
'CREATE TRIGGER %I AFTER INSERT OR UPDATE OR DELETE ON morbac.%I ' ||
|
|
'FOR EACH ROW EXECUTE FUNCTION morbac.audit_trigger()',
|
|
v_trigger_name,
|
|
p_table_name
|
|
);
|
|
|
|
RAISE NOTICE 'Audit logging enabled for morbac.%', p_table_name;
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION morbac.enable_audit(TEXT) IS
|
|
'Enable audit logging on a morbac table - creates audit trigger';
|
|
|
|
-- Helper function to disable audit logging on a table
|
|
CREATE OR REPLACE FUNCTION morbac.disable_audit(p_table_name TEXT)
|
|
RETURNS VOID
|
|
LANGUAGE plpgsql
|
|
AS $$
|
|
DECLARE
|
|
v_trigger_name TEXT;
|
|
BEGIN
|
|
v_trigger_name := 'audit_' || p_table_name;
|
|
|
|
EXECUTE format(
|
|
'DROP TRIGGER IF EXISTS %I ON morbac.%I',
|
|
v_trigger_name,
|
|
p_table_name
|
|
);
|
|
|
|
RAISE NOTICE 'Audit logging disabled for morbac.%', p_table_name;
|
|
END;
|
|
$$;
|
|
|
|
COMMENT ON FUNCTION morbac.disable_audit(TEXT) IS
|
|
'Disable audit logging on a morbac table - removes audit trigger';
|