Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const { RedisIoAdapter } = require('./websocket/redis.adapter');
const cors = require('cors');
const dotenv = require('dotenv');
const http = require('http');
const checkApiKey = require('./middleware/checkApiKey');

// Import swagger documentation
const swaggerUi = require('swagger-ui-express');
Expand All @@ -30,11 +29,17 @@ const { sequelize } = require('./database/connection');
const models = require('./models');
const { OrganizationWebhook } = models;
// Register webhook URL for organization
const { isAdminOfOrg } = require('./graphql/middleware/auth');
// Register webhook URL for organization with admin/org check
app.post('/api/admin/webhooks', async (req, res) => {
try {
const { organization_id, webhook_url } = req.body;
if (!organization_id || !webhook_url) {
return res.status(400).json({ success: false, error: 'organization_id and webhook_url are required' });
const { organization_id, webhook_url, admin_address } = req.body;
if (!organization_id || !webhook_url || !admin_address) {
return res.status(400).json({ success: false, error: 'organization_id, webhook_url, and admin_address are required' });
}
const isAdmin = await isAdminOfOrg(admin_address, organization_id);
if (!isAdmin) {
return res.status(403).json({ success: false, error: 'Forbidden: admin_address does not belong to organization' });
}
const webhook = await OrganizationWebhook.create({ organization_id, webhook_url });
res.status(201).json({ success: true, data: webhook });
Expand All @@ -52,7 +57,6 @@ const discordBotService = require('./services/discordBotService');
const cacheService = require('./services/cacheService');
const tvlService = require('./services/tvlService');
const vaultExportService = require('./services/vaultExportService');
const { rateLimitExport } = require('./util/ratelimit.utils');

// Routes
app.get('/', (req, res) => {
Expand Down Expand Up @@ -128,8 +132,6 @@ app.get('/api/claims/:userAddress/realized-gains', async (req, res) => {
});

// Admin Routes
app.use('/api/admin', checkApiKey);

app.post('/api/admin/revoke', async (req, res) => {
try {
const { adminAddress, targetVault, reason } = req.body;
Expand Down Expand Up @@ -265,27 +267,38 @@ app.get('/api/stats/tvl', async (req, res) => {
}
});

app.get('/api/vault/:id/export', rateLimitExport, async (req, res) => {
// Vault Export Routes
// Vault export with admin/org check
app.get('/api/vault/:id/export', async (req, res) => {
try {
const { id } = req.params;

const { admin_address } = req.query;
if (!admin_address) {
return res.status(400).json({ success: false, error: 'admin_address is required' });
}
// Get vault and org_id
const vault = await require('./models').Vault.findOne({ where: { id } });
if (!vault) {
return res.status(404).json({ success: false, error: 'Vault not found' });
}
const orgId = vault.org_id;
const isAdmin = await isAdminOfOrg(admin_address, orgId);
if (!isAdmin) {
return res.status(403).json({ success: false, error: 'Forbidden: admin_address does not belong to organization' });
}
// Set response headers for CSV download
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="vault-${id}-export-${new Date().toISOString().split('T')[0]}.csv"`);

// Stream the CSV data
await vaultExportService.streamVaultAsCSV(id, res);
} catch (error) {
console.error('Error exporting vault:', error);

// If headers haven't been sent yet, send JSON error response
if (!res.headersSent) {
res.status(500).json({
success: false,
error: error.message
});
} else {
// If streaming already started, destroy the stream
res.destroy(error);
}
}
Expand Down
67 changes: 67 additions & 0 deletions backend/test/crossTenantIsolation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
const axios = require('axios');
const BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';

// Replace with actual org and admin addresses for your test DB
const ORG_A = { id: 'org-a-uuid', admin: '0xadminA' };
const ORG_B = { id: 'org-b-uuid', admin: '0xadminB' };

async function testWebhookIsolation() {
console.log('🔒 Testing webhook registration isolation...');
// Admin A registers webhook for Org A (should succeed)
let res = await axios.post(`${BASE_URL}/api/admin/webhooks`, {
organization_id: ORG_A.id,
webhook_url: 'https://webhook-a.com',
admin_address: ORG_A.admin
});
if (res.status !== 201) throw new Error('Admin A failed to register webhook for Org A');
console.log('✅ Admin A registered webhook for Org A');

// Admin B tries to register webhook for Org A (should fail)
let failed = false;
try {
await axios.post(`${BASE_URL}/api/admin/webhooks`, {
organization_id: ORG_A.id,
webhook_url: 'https://webhook-b.com',
admin_address: ORG_B.admin
});
} catch (e) {
if (e.response && e.response.status === 403) {
failed = true;
console.log('✅ Admin B forbidden from registering webhook for Org A');
}
}
if (!failed) throw new Error('Admin B should not be able to register webhook for Org A');
}

async function testVaultExportIsolation() {
console.log('🔒 Testing vault export isolation...');
// Assume vaultA belongs to Org A
const vaultAId = 'vault-a-uuid';
// Admin A exports vaultA (should succeed)
let res = await axios.get(`${BASE_URL}/api/vault/${vaultAId}/export?admin_address=${ORG_A.admin}`);
if (res.status !== 200) throw new Error('Admin A failed to export vaultA');
console.log('✅ Admin A exported vaultA');

// Admin B tries to export vaultA (should fail)
let failed = false;
try {
await axios.get(`${BASE_URL}/api/vault/${vaultAId}/export?admin_address=${ORG_B.admin}`);
} catch (e) {
if (e.response && e.response.status === 403) {
failed = true;
console.log('✅ Admin B forbidden from exporting vaultA');
}
}
if (!failed) throw new Error('Admin B should not be able to export vaultA');
}

(async () => {
try {
await testWebhookIsolation();
await testVaultExportIsolation();
console.log('\n🎉 Cross-tenant isolation tests passed!');
} catch (e) {
console.error('❌ Cross-tenant isolation test failed:', e.message);
process.exit(1);
}
})();