354 lines
9.7 KiB
JavaScript
354 lines
9.7 KiB
JavaScript
/**
|
|
* Tenant Limits Middleware
|
|
* Enforces tenant subscription limits for users, devices, API rate limits, etc.
|
|
*/
|
|
|
|
const { securityLogger } = require('./logger');
|
|
|
|
// Initialize multi-tenant auth
|
|
const MultiTenantAuth = require('./multi-tenant-auth');
|
|
const multiAuth = new MultiTenantAuth();
|
|
|
|
/**
|
|
* Redis-like in-memory store for rate limiting (replace with Redis in production)
|
|
*/
|
|
class RateLimitStore {
|
|
constructor() {
|
|
this.store = new Map();
|
|
this.cleanup();
|
|
}
|
|
|
|
get(key) {
|
|
const data = this.store.get(key);
|
|
if (!data) return null;
|
|
|
|
// Check if expired
|
|
if (Date.now() > data.expires) {
|
|
this.store.delete(key);
|
|
return null;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
set(key, value, ttlMs) {
|
|
this.store.set(key, {
|
|
...value,
|
|
expires: Date.now() + ttlMs
|
|
});
|
|
}
|
|
|
|
delete(key) {
|
|
this.store.delete(key);
|
|
}
|
|
|
|
// Clean up expired entries every minute
|
|
cleanup() {
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [key, data] of this.store.entries()) {
|
|
if (now > data.expires) {
|
|
this.store.delete(key);
|
|
}
|
|
}
|
|
}, 60000);
|
|
}
|
|
}
|
|
|
|
const rateLimitStore = new RateLimitStore();
|
|
|
|
/**
|
|
* Get tenant and validate access
|
|
*/
|
|
async function getTenantFromRequest(req) {
|
|
const tenantId = await multiAuth.determineTenant(req);
|
|
if (!tenantId) {
|
|
throw new Error('Unable to determine tenant');
|
|
}
|
|
|
|
const { Tenant } = require('../models');
|
|
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
|
if (!tenant) {
|
|
throw new Error('Tenant not found');
|
|
}
|
|
|
|
return tenant;
|
|
}
|
|
|
|
/**
|
|
* Check if tenant has reached user limit
|
|
*/
|
|
async function checkUserLimit(tenantId, excludeUserId = null) {
|
|
const { User } = require('../models');
|
|
|
|
const whereClause = { tenant_id: tenantId };
|
|
if (excludeUserId) {
|
|
whereClause.id = { [require('sequelize').Op.ne]: excludeUserId };
|
|
}
|
|
|
|
const userCount = await User.count({ where: whereClause });
|
|
return userCount;
|
|
}
|
|
|
|
/**
|
|
* Check if tenant has reached device limit
|
|
*/
|
|
async function checkDeviceLimit(tenantId, excludeDeviceId = null) {
|
|
const { Device } = require('../models');
|
|
|
|
const whereClause = { tenant_id: tenantId };
|
|
if (excludeDeviceId) {
|
|
whereClause.id = { [require('sequelize').Op.ne]: excludeDeviceId };
|
|
}
|
|
|
|
const deviceCount = await Device.count({ where: whereClause });
|
|
return deviceCount;
|
|
}
|
|
|
|
/**
|
|
* Middleware to enforce user creation limits
|
|
*/
|
|
function enforceUserLimit() {
|
|
return async (req, res, next) => {
|
|
try {
|
|
const tenant = await getTenantFromRequest(req);
|
|
const maxUsers = tenant.features?.max_users;
|
|
|
|
// -1 means unlimited
|
|
if (maxUsers === -1) {
|
|
return next();
|
|
}
|
|
|
|
const currentUserCount = await checkUserLimit(tenant.id);
|
|
|
|
if (currentUserCount >= maxUsers) {
|
|
securityLogger.logSecurityEvent('warning', 'User creation blocked due to tenant limit', {
|
|
action: 'user_creation_limit_exceeded',
|
|
tenantId: tenant.id,
|
|
tenantSlug: tenant.slug,
|
|
currentUserCount,
|
|
maxUsers,
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
ip: req.ip,
|
|
userAgent: req.get('User-Agent')
|
|
});
|
|
|
|
return res.status(403).json({
|
|
success: false,
|
|
message: `Tenant has reached the maximum number of users (${maxUsers}). Please upgrade your subscription or remove existing users.`,
|
|
error_code: 'TENANT_USER_LIMIT_EXCEEDED',
|
|
current_count: currentUserCount,
|
|
max_allowed: maxUsers
|
|
});
|
|
}
|
|
|
|
next();
|
|
} catch (error) {
|
|
console.error('Error checking user limit:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to validate user limit'
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Middleware to enforce device creation limits
|
|
*/
|
|
function enforceDeviceLimit() {
|
|
return async (req, res, next) => {
|
|
try {
|
|
const tenant = await getTenantFromRequest(req);
|
|
const maxDevices = tenant.features?.max_devices;
|
|
|
|
// -1 means unlimited
|
|
if (maxDevices === -1) {
|
|
return next();
|
|
}
|
|
|
|
const currentDeviceCount = await checkDeviceLimit(tenant.id);
|
|
|
|
if (currentDeviceCount >= maxDevices) {
|
|
securityLogger.logSecurityEvent('warning', 'Device creation blocked due to tenant limit', {
|
|
action: 'device_creation_limit_exceeded',
|
|
tenantId: tenant.id,
|
|
tenantSlug: tenant.slug,
|
|
currentDeviceCount,
|
|
maxDevices,
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
ip: req.ip,
|
|
userAgent: req.get('User-Agent')
|
|
});
|
|
|
|
return res.status(403).json({
|
|
success: false,
|
|
message: `Tenant has reached the maximum number of devices (${maxDevices}). Please upgrade your subscription or remove existing devices.`,
|
|
error_code: 'TENANT_DEVICE_LIMIT_EXCEEDED',
|
|
current_count: currentDeviceCount,
|
|
max_allowed: maxDevices
|
|
});
|
|
}
|
|
|
|
next();
|
|
} catch (error) {
|
|
console.error('Error checking device limit:', error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: 'Failed to validate device limit'
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Middleware to enforce API rate limits per tenant
|
|
* Tracks actual API requests (not page views) shared among all tenant users
|
|
*/
|
|
function enforceApiRateLimit(windowMs = 60000) { // Default 1 minute window
|
|
return async (req, res, next) => {
|
|
try {
|
|
const tenant = await getTenantFromRequest(req);
|
|
const maxRequests = tenant.features?.api_rate_limit;
|
|
|
|
// -1 means unlimited
|
|
if (maxRequests === -1) {
|
|
return next();
|
|
}
|
|
|
|
const key = `api_rate_limit:${tenant.id}`;
|
|
const now = Date.now();
|
|
const windowStart = now - windowMs;
|
|
|
|
// Get current rate limit data
|
|
let rateLimitData = rateLimitStore.get(key);
|
|
|
|
if (!rateLimitData) {
|
|
rateLimitData = {
|
|
requests: [],
|
|
totalRequests: 0
|
|
};
|
|
}
|
|
|
|
// Remove old requests outside the window
|
|
rateLimitData.requests = rateLimitData.requests.filter(timestamp => timestamp > windowStart);
|
|
|
|
// Check if limit exceeded
|
|
if (rateLimitData.requests.length >= maxRequests) {
|
|
const resetTime = rateLimitData.requests[0] + windowMs;
|
|
const retryAfter = Math.ceil((resetTime - now) / 1000);
|
|
|
|
securityLogger.logSecurityEvent('warning', 'API rate limit exceeded for tenant', {
|
|
action: 'api_rate_limit_exceeded',
|
|
tenantId: tenant.id,
|
|
tenantSlug: tenant.slug,
|
|
currentRequests: rateLimitData.requests.length,
|
|
maxRequests,
|
|
windowMs,
|
|
userId: req.user?.id,
|
|
username: req.user?.username,
|
|
endpoint: req.path,
|
|
method: req.method,
|
|
ip: req.ip,
|
|
userAgent: req.get('User-Agent')
|
|
});
|
|
|
|
res.set({
|
|
'X-RateLimit-Limit': maxRequests,
|
|
'X-RateLimit-Remaining': 0,
|
|
'X-RateLimit-Reset': Math.ceil(resetTime / 1000),
|
|
'Retry-After': retryAfter
|
|
});
|
|
|
|
return res.status(429).json({
|
|
success: false,
|
|
message: `API rate limit exceeded. Maximum ${maxRequests} requests per ${windowMs/1000} seconds for your tenant.`,
|
|
error_code: 'TENANT_API_RATE_LIMIT_EXCEEDED',
|
|
max_requests: maxRequests,
|
|
window_seconds: windowMs / 1000,
|
|
retry_after_seconds: retryAfter
|
|
});
|
|
}
|
|
|
|
// Add current request
|
|
rateLimitData.requests.push(now);
|
|
rateLimitData.totalRequests++;
|
|
|
|
// Store updated data
|
|
rateLimitStore.set(key, rateLimitData, windowMs);
|
|
|
|
// Set rate limit headers
|
|
res.set({
|
|
'X-RateLimit-Limit': maxRequests,
|
|
'X-RateLimit-Remaining': Math.max(0, maxRequests - rateLimitData.requests.length),
|
|
'X-RateLimit-Reset': Math.ceil((now + windowMs) / 1000)
|
|
});
|
|
|
|
next();
|
|
} catch (error) {
|
|
console.error('Error checking API rate limit:', error);
|
|
// Don't block on rate limit errors, but log them
|
|
next();
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get tenant limits status
|
|
*/
|
|
async function getTenantLimitsStatus(tenantId) {
|
|
try {
|
|
const { Tenant } = require('../models');
|
|
const tenant = await Tenant.findByPk(tenantId);
|
|
|
|
if (!tenant) {
|
|
throw new Error('Tenant not found');
|
|
}
|
|
|
|
const [userCount, deviceCount] = await Promise.all([
|
|
checkUserLimit(tenantId),
|
|
checkDeviceLimit(tenantId)
|
|
]);
|
|
|
|
const rateLimitKey = `api_rate_limit:${tenantId}`;
|
|
const rateLimitData = rateLimitStore.get(rateLimitKey);
|
|
const currentApiRequests = rateLimitData ? rateLimitData.requests.length : 0;
|
|
|
|
return {
|
|
users: {
|
|
current: userCount,
|
|
limit: tenant.features?.max_users || 0,
|
|
unlimited: tenant.features?.max_users === -1
|
|
},
|
|
devices: {
|
|
current: deviceCount,
|
|
limit: tenant.features?.max_devices || 0,
|
|
unlimited: tenant.features?.max_devices === -1
|
|
},
|
|
api_requests: {
|
|
current_minute: currentApiRequests,
|
|
limit_per_minute: tenant.features?.api_rate_limit || 0,
|
|
unlimited: tenant.features?.api_rate_limit === -1
|
|
},
|
|
data_retention: {
|
|
days: tenant.features?.data_retention_days || 90,
|
|
unlimited: tenant.features?.data_retention_days === -1
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('Error getting tenant limits status:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
enforceUserLimit,
|
|
enforceDeviceLimit,
|
|
enforceApiRateLimit,
|
|
getTenantLimitsStatus,
|
|
checkUserLimit,
|
|
checkDeviceLimit,
|
|
rateLimitStore
|
|
}; |