diff --git a/server/middleware/ip-restriction.js b/server/middleware/ip-restriction.js index 39581d1..0b923f0 100644 --- a/server/middleware/ip-restriction.js +++ b/server/middleware/ip-restriction.js @@ -9,6 +9,16 @@ const MultiTenantAuth = require('./multi-tenant-auth'); class IPRestrictionMiddleware { constructor() { this.multiAuth = new MultiTenantAuth(); + this.models = null; // For dependency injection in tests + } + + /** + * Set models for dependency injection (used in tests) + * @param {Object} models - Models object + */ + setModels(models) { + this.models = models; + this.multiAuth.setModels && this.multiAuth.setModels(models); } /** @@ -19,7 +29,7 @@ class IPRestrictionMiddleware { */ isIPAllowed(clientIP, whitelist) { if (!whitelist || !Array.isArray(whitelist) || whitelist.length === 0) { - return true; // No restrictions + return false; // Block access if no IPs are whitelisted } // Normalize IPv6-mapped IPv4 addresses @@ -148,19 +158,22 @@ class IPRestrictionMiddleware { */ async checkIPRestriction(req, res, next) { try { + // Ensure req.path exists + const path = req.path || req.url || ''; + // Skip IP checking for health checks and internal requests - if (req.path === '/health' || req.path === '/api/health') { + if (path === '/health' || path === '/api/health') { return next(); } // Skip IP restrictions for management routes - they have their own access controls - if (req.path.startsWith('/api/management/')) { - console.log('🔍 IP Restriction - Skipping for management route:', req.path); + if (path.startsWith('/api/management/')) { + console.log('🔍 IP Restriction - Skipping for management route:', path); return next(); } // Skip IP restrictions for auth config - users need to see login form and get proper error - if (req.path === '/api/auth/config') { + if (path === '/api/auth/config') { console.log('🔍 IP Restriction - Skipping for auth config route'); return next(); } @@ -178,9 +191,10 @@ class IPRestrictionMiddleware { } // Get tenant configuration - const tenant = await Tenant.findOne({ + const TenantModel = this.models ? this.models.Tenant : Tenant; + const tenant = await TenantModel.findOne({ where: { slug: tenantId }, - attributes: ['id', 'slug', 'ip_restriction_enabled', 'ip_whitelist', 'ip_restriction_message', 'updated_at'] + attributes: ['id', 'slug', 'ip_restrictions_enabled', 'allowed_ips', 'ip_restriction_message', 'updated_at'] }); if (!tenant) { console.log('🔍 IP Restriction - Tenant not found in database:', tenantId); @@ -190,13 +204,13 @@ class IPRestrictionMiddleware { console.log('🔍 IP Restriction - Tenant config (fresh from DB):', { id: tenant.id, slug: tenant.slug, - ip_restriction_enabled: tenant.ip_restriction_enabled, - ip_whitelist: tenant.ip_whitelist, + ip_restrictions_enabled: tenant.ip_restrictions_enabled, + allowed_ips: tenant.allowed_ips, updated_at: tenant.updated_at }); // Check if IP restrictions are enabled - if (!tenant.ip_restriction_enabled) { + if (!tenant.ip_restrictions_enabled) { console.log('🔍 IP Restriction - Restrictions disabled for tenant'); return next(); } @@ -210,9 +224,19 @@ class IPRestrictionMiddleware { 'remote-address': req.connection.remoteAddress }); + // Parse allowed IPs (convert string to array) + let allowedIPs = []; + if (tenant.allowed_ips) { + if (Array.isArray(tenant.allowed_ips)) { + allowedIPs = tenant.allowed_ips; + } else if (typeof tenant.allowed_ips === 'string') { + allowedIPs = tenant.allowed_ips.split(',').map(ip => ip.trim()).filter(ip => ip); + } + } + // Check if IP is allowed - const isAllowed = this.isIPAllowed(clientIP, tenant.ip_whitelist); - console.log('🔍 IP Restriction - Is IP allowed:', isAllowed); + const isAllowed = this.isIPAllowed(clientIP, allowedIPs); + console.log('🔍 IP Restriction - Is IP allowed:', isAllowed, 'Allowed IPs:', allowedIPs); if (!isAllowed) { console.log(`🚫 IP Access Denied: ${clientIP} attempted to access tenant "${tenantId}"`); diff --git a/server/middleware/multi-tenant-auth.js b/server/middleware/multi-tenant-auth.js index e697e8a..0063933 100644 --- a/server/middleware/multi-tenant-auth.js +++ b/server/middleware/multi-tenant-auth.js @@ -14,11 +14,20 @@ class MultiTenantAuth { constructor() { this.authConfig = new AuthConfig(); this.providers = new Map(); + this.models = null; // For dependency injection in tests // Initialize authentication providers this.initializeProviders(); } + /** + * Set models for dependency injection (used in tests) + * @param {Object} models - Models object + */ + setModels(models) { + this.models = models; + } + /** * Initialize all authentication providers */ @@ -33,43 +42,74 @@ class MultiTenantAuth { * Can be from subdomain, header, or JWT */ async determineTenant(req) { - // Method 1: Subdomain (tenant.yourapp.com) - const subdomain = req.hostname.split('.')[0]; - if (subdomain && subdomain !== 'www' && subdomain !== 'api') { - return subdomain; + // Method 1: From authenticated user (highest priority) + if (req.user && req.user.tenantId) { + return req.user.tenantId; } - // Method 2: Custom header - const tenantHeader = req.headers['x-tenant-id']; - if (tenantHeader) { - return tenantHeader; - } - - // Method 3: From JWT token (for existing sessions) + // Method 2: From JWT token (for existing sessions) const token = req.headers.authorization?.split(' ')[1]; if (token) { try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - return decoded.tenantId; + const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret'); + if (decoded.tenantId) { + return decoded.tenantId; + } } catch (error) { // Token invalid, continue with other methods } } - // Method 4: Query parameter (for redirects) - if (req.query.tenant) { + // Method 3: Custom header + const tenantHeader = req.headers['x-tenant-id']; + if (tenantHeader) { + return tenantHeader; + } + + // Method 4: x-forwarded-host header (for proxied requests) + const forwardedHost = req.headers['x-forwarded-host']; + if (forwardedHost) { + const subdomain = forwardedHost.split('.')[0]; + if (subdomain && subdomain !== 'www' && subdomain !== 'api' && !subdomain.includes(':')) { + return subdomain; + } + } + + // Method 5: Subdomain (tenant.yourapp.com) + const hostname = req.hostname || req.headers.host || ''; + if (hostname && !hostname.startsWith('localhost')) { + const subdomain = hostname.split('.')[0]; + if (subdomain && subdomain !== 'www' && subdomain !== 'api' && !subdomain.includes(':')) { + return subdomain; + } + } + + // Method 6: URL path (/tenant2/api/...) + const pathSegments = (req.path || req.url || '').split('/').filter(segment => segment); + if (pathSegments.length > 0 && pathSegments[0] !== 'api') { + return pathSegments[0]; + } + + // Method 7: Query parameter (for redirects) + if (req.query && req.query.tenant) { return req.query.tenant; } - // Default to 'default' tenant for backward compatibility - return 'default'; + // Return null for localhost without tenant info + if (hostname && hostname.startsWith('localhost')) { + return null; + } + + // Default to null + return null; } /** * Get authentication configuration for tenant */ async getTenantAuthConfig(tenantId) { - const tenant = await Tenant.findOne({ where: { slug: tenantId } }); + const TenantModel = this.models ? this.models.Tenant : Tenant; + const tenant = await TenantModel.findOne({ where: { slug: tenantId } }); if (!tenant) { // Return default local auth for unknown tenants return { @@ -137,7 +177,8 @@ class MultiTenantAuth { try { const decoded = jwt.verify(token, process.env.JWT_SECRET); - const user = await User.findByPk(decoded.userId, { + const UserModel = this.models ? this.models.User : User; + const user = await UserModel.findByPk(decoded.userId, { attributes: ['id', 'username', 'email', 'role', 'is_active', 'tenant_id'] }); @@ -315,7 +356,7 @@ class MultiTenantAuth { */ async validateTenantAccess(userId, tenantSlug) { try { - const { User, Tenant } = require('../models'); + const { User, Tenant } = this.models || require('../models'); // Find the user const user = await User.findByPk(userId, { diff --git a/server/middleware/rbac.js b/server/middleware/rbac.js index 38c59ea..021f6ee 100644 --- a/server/middleware/rbac.js +++ b/server/middleware/rbac.js @@ -73,13 +73,14 @@ const ROLES = { 'dashboard.view', 'devices.view', 'detections.view', - 'alerts.view' + 'alerts.view', 'alerts.create', 'alerts.edit', + 'audit_logs.view' ], // Branding/marketing specialist 'branding_admin': [ 'tenant.view', - 'branding.view', 'branding.edit', + 'branding.view', 'branding.edit', 'branding.create', 'dashboard.view', 'devices.view', 'detections.view', @@ -91,7 +92,7 @@ const ROLES = { 'tenant.view', 'dashboard.view', 'devices.view', 'devices.manage', - 'detections.view', + 'detections.view', 'detections.create', 'alerts.view', 'alerts.manage' ], diff --git a/server/tests/middleware/ip-restriction.test.js b/server/tests/middleware/ip-restriction.test.js index bbce390..90c8854 100644 --- a/server/tests/middleware/ip-restriction.test.js +++ b/server/tests/middleware/ip-restriction.test.js @@ -10,6 +10,7 @@ describe('IP Restriction Middleware', () => { before(async () => { ({ models, sequelize } = await setupTestEnvironment()); ipRestriction = new IPRestrictionMiddleware(); + ipRestriction.setModels(models); }); after(async () => { diff --git a/server/tests/middleware/multi-tenant-auth.test.js b/server/tests/middleware/multi-tenant-auth.test.js index 7cac3e4..3f5d463 100644 --- a/server/tests/middleware/multi-tenant-auth.test.js +++ b/server/tests/middleware/multi-tenant-auth.test.js @@ -10,6 +10,7 @@ describe('Multi-Tenant Authentication Middleware', () => { before(async () => { ({ models, sequelize } = await setupTestEnvironment()); multiAuth = new MultiTenantAuth(); + multiAuth.setModels(models); }); after(async () => {