Files
AdPlatform-Server/Gateway/Migrations/SecurityHardening.sql
2026-03-14 13:50:09 -07:00

175 lines
7.5 KiB
Transact-SQL

-- ════════════════════════════════════════════════════════════════
-- SECURITY HARDENING: Stored Procedure Ownership Enforcement
-- ════════════════════════════════════════════════════════════════
--
-- PURPOSE: Add WHERE clientId checks to all stored procedures
-- that accept initiativeId, channelCampaignId, or wizardId.
--
-- The Gateway now passes clientId in all JSON requests.
-- These proc changes enforce ownership at the database level
-- as a SECOND layer of defense (the Gateway guard is the first).
--
-- APPLY: Run against your AdPlatform SQL Server database.
-- TEST FIRST in dev/staging before production.
-- ════════════════════════════════════════════════════════════════
-- ──────────────────────────────────────────────────
-- PATTERN: Inside each proc's @Action handler, add:
--
-- DECLARE @clientId NVARCHAR(100) = JSON_VALUE(@Rqst, '$.clientId');
--
-- -- Then in every SELECT/UPDATE/DELETE that references an initiative:
-- WHERE i.initiativeId = @initiativeId
-- AND i.clientId = @clientId -- ← ADD THIS
--
-- -- If the WHERE filters out the row, return "not found"
-- -- (same response as non-existent ID — prevents enumeration)
-- ──────────────────────────────────────────────────
PRINT '=== Security Hardening Migration ==='
PRINT ''
-- ──────────────────────────────────────────────────
-- 1. spInitiative — get, update, updateStatus, delete
-- ──────────────────────────────────────────────────
PRINT 'Hardening spInitiative...'
-- Example pattern for the "get" action:
-- (Apply this pattern to get, update, updateStatus, delete actions)
--
-- Current:
-- SELECT ... FROM tbInitiative WHERE initiativeId = @initiativeId
--
-- Hardened:
-- DECLARE @clientId NVARCHAR(100) = JSON_VALUE(@Rqst, '$.clientId')
-- SELECT ... FROM tbInitiative
-- WHERE initiativeId = @initiativeId
-- AND (@clientId IS NULL OR clientId = @clientId)
--
-- The @clientId IS NULL fallback allows internal/system calls
-- (like InitiativeLaunchService) that don't pass clientId to still work.
-- IMPORTANT: Apply to each action in spInitiative:
-- 'get' → WHERE initiativeId = @id AND (@clientId IS NULL OR clientId = @clientId)
-- 'update' → same
-- 'updateStatus' → same
-- 'delete' → same
-- 'list' → already scoped by clientId (verify it uses = not LIKE)
GO
-- ──────────────────────────────────────────────────
-- 2. spChannelCampaign — get, sync
-- ──────────────────────────────────────────────────
PRINT 'Hardening spChannelCampaign...'
-- Channel campaigns link to initiatives, so ownership requires a JOIN:
--
-- Current:
-- SELECT cc.* FROM tbChannelCampaign cc WHERE cc.channelCampaignId = @id
--
-- Hardened:
-- SELECT cc.*
-- FROM tbChannelCampaign cc
-- JOIN tbInitiative i ON cc.initiativeId = i.initiativeId
-- WHERE cc.channelCampaignId = @id
-- AND (@clientId IS NULL OR i.clientId = @clientId)
--
-- For 'sync' action: This is now admin-only in the Gateway,
-- but add the JOIN anyway for defense in depth.
GO
-- ──────────────────────────────────────────────────
-- 3. spCampaignWizard — get, updateStep, setStep, submit, updateStatus, delete
-- ──────────────────────────────────────────────────
PRINT 'Hardening spCampaignWizard...'
-- Current:
-- SELECT ... FROM tbCampaignWizard WHERE wizardId = @wizardId
--
-- Hardened:
-- DECLARE @clientId NVARCHAR(100) = JSON_VALUE(@Rqst, '$.clientId')
-- SELECT ... FROM tbCampaignWizard
-- WHERE wizardId = @wizardId
-- AND (@clientId IS NULL OR clientId = @clientId)
GO
-- ──────────────────────────────────────────────────
-- 4. spAllocation — all actions
-- ──────────────────────────────────────────────────
PRINT 'Hardening spAllocation...'
-- Allocations link to initiatives:
--
-- Hardened:
-- JOIN tbInitiative i ON a.initiativeId = i.initiativeId
-- WHERE a.initiativeId = @initiativeId
-- AND (@clientId IS NULL OR i.clientId = @clientId)
GO
-- ──────────────────────────────────────────────────
-- 5. Status transition validation at DB level (optional extra layer)
-- ──────────────────────────────────────────────────
PRINT 'Adding status transition function...'
-- Create a function the procs can call to validate transitions:
IF OBJECT_ID('dbo.fnIsValidStatusTransition', 'FN') IS NOT NULL
DROP FUNCTION dbo.fnIsValidStatusTransition
GO
CREATE FUNCTION dbo.fnIsValidStatusTransition(
@currentStatus VARCHAR(20),
@requestedStatus VARCHAR(20),
@isSystem BIT = 0 -- 1 = system/admin (broader transitions allowed)
)
RETURNS BIT
AS
BEGIN
-- System can do anything
IF @isSystem = 1 RETURN 1
-- Client-allowed transitions
IF @currentStatus = 'active' AND @requestedStatus = 'paused' RETURN 1
IF @currentStatus = 'paused' AND @requestedStatus = 'active' RETURN 1
IF @currentStatus IN ('draft','staged','pending','active','paused')
AND @requestedStatus = 'cancelled' RETURN 1
RETURN 0
END
GO
-- ──────────────────────────────────────────────────
-- 6. spGoogleAccount — validate
-- ──────────────────────────────────────────────────
PRINT 'Verifying spGoogleAccount...'
-- The validate action should verify that the customerId
-- belongs to the requesting client. Current implementation
-- may not check this — verify and add:
--
-- WHERE a.customerId = @customerId
-- AND a.clientId = @clientId
GO
PRINT ''
PRINT '=== Migration complete ==='
PRINT 'NOTE: This is a TEMPLATE. Review each stored procedure and apply'
PRINT 'the ownership WHERE clauses to match your exact table/column names.'
PRINT ''
PRINT 'After applying, test:'
PRINT ' 1. Normal user can only see their own initiatives/wizards'
PRINT ' 2. User A cannot access User B resources by guessing IDs'
PRINT ' 3. LaunchService (no clientId) can still read initiatives'
PRINT ' 4. Admin role can sync channel status'
GO