175 lines
7.5 KiB
Transact-SQL
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
|