namespace GB5Shared.Validation { // Verifies that a record a caller is about to attach a child row to (e.g. attaching a new // ContentContainer to an existing ContentId) actually belongs to the caller's own tenant. // A tenant-scoped WHERE clause on an UPDATE/DELETE already blocks cross-tenant writes to a // row the caller doesn't own, but it does nothing to stop a caller from pointing a brand-new // child row at a PARENT id belonging to a different tenant — that parent lookup has to be // checked explicitly. See CLAUDE.md's multi-tenancy rule: cross-tenant data access is a // security violation regardless of which direction the FK points. public interface ICrossTenantGuard { void EnsureOwnedByTenant(int? actualOwnerTenantId, int callerTenantId, string entityName, object entityId); } public class CrossTenantGuard : ICrossTenantGuard { public void EnsureOwnedByTenant(int? actualOwnerTenantId, int callerTenantId, string entityName, object entityId) { if (actualOwnerTenantId is null) throw new UnauthorizedAccessException($"{entityName} '{entityId}' was not found."); if (actualOwnerTenantId.Value != callerTenantId) throw new UnauthorizedAccessException($"{entityName} '{entityId}' does not belong to the caller's tenant."); } } }