re-uploading work
This commit is contained in:
362
migrations/20260120000002_webhook_advanced_features.sql
Normal file
362
migrations/20260120000002_webhook_advanced_features.sql
Normal file
@@ -0,0 +1,362 @@
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user