Files
attune/migrations.old/20260120000002_webhook_advanced_features.sql

363 lines
13 KiB
PL/PgSQL

-- Migration: Add advanced webhook features (HMAC, rate limiting, IP whitelist)
-- Created: 2026-01-20
-- Phase: 3 - Advanced Security Features
-- Add advanced webhook configuration columns to trigger table
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_hmac_enabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_hmac_secret VARCHAR(128);
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_hmac_algorithm VARCHAR(32) DEFAULT 'sha256';
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_rate_limit_enabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_rate_limit_requests INTEGER DEFAULT 100;
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_rate_limit_window_seconds INTEGER DEFAULT 60;
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_ip_whitelist_enabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_ip_whitelist TEXT[]; -- Array of IP addresses/CIDR blocks
ALTER TABLE trigger ADD COLUMN IF NOT EXISTS
webhook_payload_size_limit_kb INTEGER DEFAULT 1024; -- Default 1MB
COMMENT ON COLUMN trigger.webhook_hmac_enabled IS 'Whether HMAC signature verification is required';
COMMENT ON COLUMN trigger.webhook_hmac_secret IS 'Secret key for HMAC signature verification';
COMMENT ON COLUMN trigger.webhook_hmac_algorithm IS 'HMAC algorithm (sha256, sha512, etc.)';
COMMENT ON COLUMN trigger.webhook_rate_limit_enabled IS 'Whether rate limiting is enabled';
COMMENT ON COLUMN trigger.webhook_rate_limit_requests IS 'Max requests allowed per window';
COMMENT ON COLUMN trigger.webhook_rate_limit_window_seconds IS 'Rate limit time window in seconds';
COMMENT ON COLUMN trigger.webhook_ip_whitelist_enabled IS 'Whether IP whitelist is enabled';
COMMENT ON COLUMN trigger.webhook_ip_whitelist IS 'Array of allowed IP addresses/CIDR blocks';
COMMENT ON COLUMN trigger.webhook_payload_size_limit_kb IS 'Maximum webhook payload size in KB';
-- Create webhook event log table for auditing and analytics
CREATE TABLE IF NOT EXISTS webhook_event_log (
id BIGSERIAL PRIMARY KEY,
trigger_id BIGINT NOT NULL REFERENCES trigger(id) ON DELETE CASCADE,
trigger_ref VARCHAR(255) NOT NULL,
webhook_key VARCHAR(64) NOT NULL,
event_id BIGINT REFERENCES event(id) ON DELETE SET NULL,
source_ip INET,
user_agent TEXT,
payload_size_bytes INTEGER,
headers JSONB,
status_code INTEGER NOT NULL,
error_message TEXT,
processing_time_ms INTEGER,
hmac_verified BOOLEAN,
rate_limited BOOLEAN DEFAULT FALSE,
ip_allowed BOOLEAN,
created TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_webhook_event_log_trigger_id ON webhook_event_log(trigger_id);
CREATE INDEX idx_webhook_event_log_webhook_key ON webhook_event_log(webhook_key);
CREATE INDEX idx_webhook_event_log_created ON webhook_event_log(created DESC);
CREATE INDEX idx_webhook_event_log_status ON webhook_event_log(status_code);
CREATE INDEX idx_webhook_event_log_source_ip ON webhook_event_log(source_ip);
COMMENT ON TABLE webhook_event_log IS 'Audit log of all webhook requests';
COMMENT ON COLUMN webhook_event_log.status_code IS 'HTTP status code returned (200, 400, 403, 429, etc.)';
COMMENT ON COLUMN webhook_event_log.error_message IS 'Error message if request failed';
COMMENT ON COLUMN webhook_event_log.processing_time_ms IS 'Time taken to process webhook in milliseconds';
COMMENT ON COLUMN webhook_event_log.hmac_verified IS 'Whether HMAC signature was verified successfully';
COMMENT ON COLUMN webhook_event_log.rate_limited IS 'Whether request was rate limited';
COMMENT ON COLUMN webhook_event_log.ip_allowed IS 'Whether source IP was in whitelist (if enabled)';
-- Create webhook rate limit tracking table
CREATE TABLE IF NOT EXISTS webhook_rate_limit (
id BIGSERIAL PRIMARY KEY,
webhook_key VARCHAR(64) NOT NULL,
window_start TIMESTAMPTZ NOT NULL,
request_count INTEGER NOT NULL DEFAULT 1,
created TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(webhook_key, window_start)
);
CREATE INDEX idx_webhook_rate_limit_key ON webhook_rate_limit(webhook_key);
CREATE INDEX idx_webhook_rate_limit_window ON webhook_rate_limit(window_start DESC);
COMMENT ON TABLE webhook_rate_limit IS 'Tracks webhook request counts for rate limiting';
COMMENT ON COLUMN webhook_rate_limit.window_start IS 'Start of the rate limit time window';
COMMENT ON COLUMN webhook_rate_limit.request_count IS 'Number of requests in this window';
-- Function to generate HMAC secret
CREATE OR REPLACE FUNCTION generate_webhook_hmac_secret()
RETURNS VARCHAR(128) AS $$
DECLARE
secret VARCHAR(128);
BEGIN
-- Generate 64-byte (128 hex chars) random secret
SELECT encode(gen_random_bytes(64), 'hex') INTO secret;
RETURN secret;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION generate_webhook_hmac_secret() IS 'Generate a secure random HMAC secret';
-- Function to enable HMAC for a trigger
CREATE OR REPLACE FUNCTION enable_trigger_webhook_hmac(
p_trigger_id BIGINT,
p_algorithm VARCHAR(32) DEFAULT 'sha256'
)
RETURNS TABLE(
webhook_hmac_enabled BOOLEAN,
webhook_hmac_secret VARCHAR(128),
webhook_hmac_algorithm VARCHAR(32)
) AS $$
DECLARE
v_webhook_enabled BOOLEAN;
v_secret VARCHAR(128);
BEGIN
-- Check if webhooks are enabled
SELECT t.webhook_enabled INTO v_webhook_enabled
FROM trigger t
WHERE t.id = p_trigger_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Trigger with id % not found', p_trigger_id;
END IF;
IF NOT v_webhook_enabled THEN
RAISE EXCEPTION 'Webhooks must be enabled before enabling HMAC verification';
END IF;
-- Validate algorithm
IF p_algorithm NOT IN ('sha256', 'sha512', 'sha1') THEN
RAISE EXCEPTION 'Invalid HMAC algorithm. Supported: sha256, sha512, sha1';
END IF;
-- Generate new secret
v_secret := generate_webhook_hmac_secret();
-- Update trigger
UPDATE trigger
SET
webhook_hmac_enabled = TRUE,
webhook_hmac_secret = v_secret,
webhook_hmac_algorithm = p_algorithm,
updated = NOW()
WHERE id = p_trigger_id;
-- Return result
RETURN QUERY
SELECT
TRUE AS webhook_hmac_enabled,
v_secret AS webhook_hmac_secret,
p_algorithm AS webhook_hmac_algorithm;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION enable_trigger_webhook_hmac(BIGINT, VARCHAR) IS 'Enable HMAC signature verification for a trigger';
-- Function to disable HMAC for a trigger
CREATE OR REPLACE FUNCTION disable_trigger_webhook_hmac(p_trigger_id BIGINT)
RETURNS BOOLEAN AS $$
BEGIN
UPDATE trigger
SET
webhook_hmac_enabled = FALSE,
webhook_hmac_secret = NULL,
updated = NOW()
WHERE id = p_trigger_id;
RETURN FOUND;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION disable_trigger_webhook_hmac(BIGINT) IS 'Disable HMAC verification for a trigger';
-- Function to configure rate limiting
CREATE OR REPLACE FUNCTION configure_trigger_webhook_rate_limit(
p_trigger_id BIGINT,
p_enabled BOOLEAN,
p_requests INTEGER DEFAULT 100,
p_window_seconds INTEGER DEFAULT 60
)
RETURNS TABLE(
rate_limit_enabled BOOLEAN,
rate_limit_requests INTEGER,
rate_limit_window_seconds INTEGER
) AS $$
BEGIN
-- Validate inputs
IF p_requests < 1 OR p_requests > 10000 THEN
RAISE EXCEPTION 'Rate limit requests must be between 1 and 10000';
END IF;
IF p_window_seconds < 1 OR p_window_seconds > 3600 THEN
RAISE EXCEPTION 'Rate limit window must be between 1 and 3600 seconds';
END IF;
-- Update trigger
UPDATE trigger
SET
webhook_rate_limit_enabled = p_enabled,
webhook_rate_limit_requests = p_requests,
webhook_rate_limit_window_seconds = p_window_seconds,
updated = NOW()
WHERE id = p_trigger_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Trigger with id % not found', p_trigger_id;
END IF;
-- Return configuration
RETURN QUERY
SELECT
p_enabled AS rate_limit_enabled,
p_requests AS rate_limit_requests,
p_window_seconds AS rate_limit_window_seconds;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION configure_trigger_webhook_rate_limit(BIGINT, BOOLEAN, INTEGER, INTEGER) IS 'Configure rate limiting for a trigger webhook';
-- Function to configure IP whitelist
CREATE OR REPLACE FUNCTION configure_trigger_webhook_ip_whitelist(
p_trigger_id BIGINT,
p_enabled BOOLEAN,
p_ip_list TEXT[] DEFAULT ARRAY[]::TEXT[]
)
RETURNS TABLE(
ip_whitelist_enabled BOOLEAN,
ip_whitelist TEXT[]
) AS $$
BEGIN
-- Update trigger
UPDATE trigger
SET
webhook_ip_whitelist_enabled = p_enabled,
webhook_ip_whitelist = p_ip_list,
updated = NOW()
WHERE id = p_trigger_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'Trigger with id % not found', p_trigger_id;
END IF;
-- Return configuration
RETURN QUERY
SELECT
p_enabled AS ip_whitelist_enabled,
p_ip_list AS ip_whitelist;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION configure_trigger_webhook_ip_whitelist(BIGINT, BOOLEAN, TEXT[]) IS 'Configure IP whitelist for a trigger webhook';
-- Function to check rate limit (call before processing webhook)
CREATE OR REPLACE FUNCTION check_webhook_rate_limit(
p_webhook_key VARCHAR(64),
p_max_requests INTEGER,
p_window_seconds INTEGER
)
RETURNS BOOLEAN AS $$
DECLARE
v_window_start TIMESTAMPTZ;
v_request_count INTEGER;
BEGIN
-- Calculate current window start (truncated to window boundary)
v_window_start := date_trunc('minute', NOW()) -
((EXTRACT(EPOCH FROM date_trunc('minute', NOW()))::INTEGER % p_window_seconds) || ' seconds')::INTERVAL;
-- Get or create rate limit record
INSERT INTO webhook_rate_limit (webhook_key, window_start, request_count)
VALUES (p_webhook_key, v_window_start, 1)
ON CONFLICT (webhook_key, window_start)
DO UPDATE SET
request_count = webhook_rate_limit.request_count + 1,
updated = NOW()
RETURNING request_count INTO v_request_count;
-- Clean up old rate limit records (older than 1 hour)
DELETE FROM webhook_rate_limit
WHERE window_start < NOW() - INTERVAL '1 hour';
-- Return TRUE if within limit, FALSE if exceeded
RETURN v_request_count <= p_max_requests;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION check_webhook_rate_limit(VARCHAR, INTEGER, INTEGER) IS 'Check if webhook request is within rate limit';
-- Function to check if IP is in whitelist (supports CIDR notation)
CREATE OR REPLACE FUNCTION check_webhook_ip_whitelist(
p_source_ip INET,
p_whitelist TEXT[]
)
RETURNS BOOLEAN AS $$
DECLARE
v_allowed_cidr TEXT;
BEGIN
-- If whitelist is empty, deny access
IF p_whitelist IS NULL OR array_length(p_whitelist, 1) IS NULL THEN
RETURN FALSE;
END IF;
-- Check if source IP matches any entry in whitelist
FOREACH v_allowed_cidr IN ARRAY p_whitelist
LOOP
-- Handle both single IPs and CIDR notation
IF p_source_ip <<= v_allowed_cidr::INET THEN
RETURN TRUE;
END IF;
END LOOP;
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION check_webhook_ip_whitelist(INET, TEXT[]) IS 'Check if source IP is in whitelist (supports CIDR notation)';
-- View for webhook statistics
CREATE OR REPLACE VIEW webhook_stats_detailed AS
SELECT
t.id AS trigger_id,
t.ref AS trigger_ref,
t.label AS trigger_label,
t.webhook_enabled,
t.webhook_key,
t.webhook_hmac_enabled,
t.webhook_rate_limit_enabled,
t.webhook_rate_limit_requests,
t.webhook_rate_limit_window_seconds,
t.webhook_ip_whitelist_enabled,
COUNT(DISTINCT wel.id) AS total_requests,
COUNT(DISTINCT wel.id) FILTER (WHERE wel.status_code = 200) AS successful_requests,
COUNT(DISTINCT wel.id) FILTER (WHERE wel.status_code >= 400) AS failed_requests,
COUNT(DISTINCT wel.id) FILTER (WHERE wel.rate_limited = TRUE) AS rate_limited_requests,
COUNT(DISTINCT wel.id) FILTER (WHERE wel.hmac_verified = FALSE AND t.webhook_hmac_enabled = TRUE) AS hmac_failures,
COUNT(DISTINCT wel.id) FILTER (WHERE wel.ip_allowed = FALSE AND t.webhook_ip_whitelist_enabled = TRUE) AS ip_blocked_requests,
COUNT(DISTINCT wel.event_id) AS events_created,
AVG(wel.processing_time_ms) AS avg_processing_time_ms,
MAX(wel.created) AS last_request_at,
t.created AS webhook_enabled_at
FROM trigger t
LEFT JOIN webhook_event_log wel ON wel.trigger_id = t.id
WHERE t.webhook_enabled = TRUE
GROUP BY t.id, t.ref, t.label, t.webhook_enabled, t.webhook_key,
t.webhook_hmac_enabled, t.webhook_rate_limit_enabled,
t.webhook_rate_limit_requests, t.webhook_rate_limit_window_seconds,
t.webhook_ip_whitelist_enabled, t.created;
COMMENT ON VIEW webhook_stats_detailed IS 'Detailed statistics for webhook-enabled triggers';
-- Grant permissions (adjust as needed for your security model)
GRANT SELECT, INSERT ON webhook_event_log TO attune_api;
GRANT SELECT, INSERT, UPDATE, DELETE ON webhook_rate_limit TO attune_api;
GRANT SELECT ON webhook_stats_detailed TO attune_api;
GRANT USAGE, SELECT ON SEQUENCE webhook_event_log_id_seq TO attune_api;
GRANT USAGE, SELECT ON SEQUENCE webhook_rate_limit_id_seq TO attune_api;