377 lines
12 KiB
JavaScript
377 lines
12 KiB
JavaScript
const express = require('express');
|
||
const router = express.Router();
|
||
const http = require('http');
|
||
const jwt = require('jsonwebtoken');
|
||
const { ManagementUser } = require('../models');
|
||
const auditLogger = require('../utils/dataRetentionAuditLogger');
|
||
|
||
/**
|
||
* Data Retention Metrics Proxy
|
||
* Proxies requests to the data retention microservice
|
||
* RESTRICTED ACCESS: Management users only
|
||
*/
|
||
|
||
const DATA_RETENTION_HOST = process.env.DATA_RETENTION_HOST || 'data-retention';
|
||
const DATA_RETENTION_PORT = process.env.DATA_RETENTION_PORT || 3001;
|
||
|
||
/**
|
||
* Management authentication middleware
|
||
* Only allows authenticated management users with proper permissions
|
||
*/
|
||
const requireManagementAuth = async (req, res, next) => {
|
||
try {
|
||
// Check for management auth token
|
||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||
|
||
if (!token) {
|
||
await auditLogger.logAuthFailure(req, req.path, 'No authentication token provided');
|
||
return res.status(401).json({
|
||
success: false,
|
||
error: 'Management authentication required',
|
||
message: 'This endpoint requires management portal authentication'
|
||
});
|
||
}
|
||
|
||
// Verify JWT token
|
||
let decoded;
|
||
try {
|
||
decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production');
|
||
} catch (jwtError) {
|
||
await auditLogger.logAuthFailure(req, req.path, `Invalid JWT token: ${jwtError.message}`);
|
||
return res.status(401).json({
|
||
success: false,
|
||
error: 'Invalid management token',
|
||
message: 'Authentication token is invalid or expired'
|
||
});
|
||
}
|
||
|
||
// Verify this is a management user
|
||
if (decoded.type !== 'management') {
|
||
await auditLogger.logPermissionDenied(null, req, req.path, 'Not a management user token');
|
||
return res.status(403).json({
|
||
success: false,
|
||
error: 'Management access required',
|
||
message: 'This endpoint requires management portal privileges'
|
||
});
|
||
}
|
||
|
||
// Verify user still exists and is active
|
||
const managementUser = await ManagementUser.findByPk(decoded.id);
|
||
if (!managementUser || !managementUser.is_active) {
|
||
await auditLogger.logPermissionDenied(decoded, req, req.path, 'Management user not found or inactive');
|
||
return res.status(403).json({
|
||
success: false,
|
||
error: 'Management user not found or inactive',
|
||
message: 'Management user account is not active'
|
||
});
|
||
}
|
||
|
||
// Check if user has permission to access system metrics
|
||
const hasMetricsAccess = managementUser.role === 'super_admin' ||
|
||
(managementUser.permissions && managementUser.permissions.includes('system_metrics'));
|
||
|
||
if (!hasMetricsAccess) {
|
||
await auditLogger.logPermissionDenied(managementUser, req, req.path, 'Insufficient permissions for system metrics');
|
||
return res.status(403).json({
|
||
success: false,
|
||
error: 'Insufficient permissions',
|
||
message: 'This endpoint requires system metrics permissions'
|
||
});
|
||
}
|
||
|
||
// Log access for security audit
|
||
console.log(`📊 Data retention metrics accessed by management user: ${managementUser.username} (${managementUser.role})`);
|
||
|
||
req.managementUser = managementUser;
|
||
next();
|
||
|
||
} catch (error) {
|
||
console.error('Management auth error:', error);
|
||
res.status(500).json({
|
||
success: false,
|
||
error: 'Authentication error',
|
||
message: 'Failed to verify management authentication'
|
||
});
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Network access control for data retention metrics
|
||
* Allow authenticated management users (authentication is primary security)
|
||
*/
|
||
const requireManagementNetwork = async (req, res, next) => {
|
||
// Since this endpoint is already protected by management authentication,
|
||
// we can be more permissive with network access for legitimate management users
|
||
|
||
// Skip network restrictions if user is already authenticated as management
|
||
if (req.managementUser) {
|
||
return next();
|
||
}
|
||
|
||
const clientIP = req.ip || req.connection.remoteAddress;
|
||
const forwardedFor = req.headers['x-forwarded-for'];
|
||
|
||
// Allow internal Docker network and common management access patterns
|
||
const allowedNetworks = [
|
||
'127.0.0.1',
|
||
'::1',
|
||
'localhost',
|
||
// Docker internal networks
|
||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Docker default bridge
|
||
/^10\./, // Docker custom networks
|
||
/^192\.168\./ // Local networks
|
||
];
|
||
|
||
// Check if request is from allowed internal network
|
||
const isAllowed = allowedNetworks.some(network => {
|
||
if (typeof network === 'string') {
|
||
return clientIP === network || forwardedFor === network;
|
||
} else {
|
||
return network.test(clientIP) || (forwardedFor && network.test(forwardedFor));
|
||
}
|
||
});
|
||
|
||
if (!isAllowed) {
|
||
// For external IPs, rely on management authentication only
|
||
// Log the attempt but don't block if user has valid management credentials
|
||
console.log(`<EFBFBD> Data retention access from external IP: ${clientIP} (forwarded: ${forwardedFor}) - will rely on authentication`);
|
||
}
|
||
|
||
next();
|
||
};
|
||
|
||
/**
|
||
* Make HTTP request to data retention service
|
||
*/
|
||
function makeRequest(path) {
|
||
return new Promise((resolve, reject) => {
|
||
const options = {
|
||
hostname: DATA_RETENTION_HOST,
|
||
port: DATA_RETENTION_PORT,
|
||
path: path,
|
||
method: 'GET',
|
||
timeout: 5000
|
||
};
|
||
|
||
const req = http.request(options, (res) => {
|
||
let data = '';
|
||
|
||
res.on('data', (chunk) => {
|
||
data += chunk;
|
||
});
|
||
|
||
res.on('end', () => {
|
||
try {
|
||
const result = JSON.parse(data);
|
||
resolve({ status: res.statusCode, data: result });
|
||
} catch (error) {
|
||
reject(new Error(`Failed to parse response: ${error.message}`));
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', (error) => {
|
||
reject(new Error(`Request failed: ${error.message}`));
|
||
});
|
||
|
||
req.on('timeout', () => {
|
||
req.destroy();
|
||
reject(new Error('Request timeout'));
|
||
});
|
||
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Apply security middleware to all data retention routes
|
||
*/
|
||
router.use(requireManagementNetwork);
|
||
router.use(requireManagementAuth);
|
||
|
||
/**
|
||
* GET /api/data-retention/metrics
|
||
* Get detailed metrics from data retention service
|
||
* RESTRICTED: Management users only
|
||
*/
|
||
router.get('/metrics', async (req, res) => {
|
||
try {
|
||
// Add security headers
|
||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||
res.setHeader('X-Frame-Options', 'DENY');
|
||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||
|
||
const response = await makeRequest('/metrics');
|
||
|
||
if (response.status === 200) {
|
||
// Log successful access
|
||
await auditLogger.logSuccess(req.managementUser, req, '/metrics');
|
||
console.log(`✅ Data retention metrics accessed by ${req.managementUser.username}`);
|
||
|
||
res.json({
|
||
success: true,
|
||
data: response.data,
|
||
timestamp: new Date().toISOString(),
|
||
accessedBy: {
|
||
username: req.managementUser.username,
|
||
role: req.managementUser.role
|
||
}
|
||
});
|
||
} else {
|
||
res.status(response.status).json({
|
||
success: false,
|
||
error: 'Failed to fetch metrics from data retention service'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ Data retention metrics error for ${req.managementUser.username}:`, error);
|
||
res.status(503).json({
|
||
success: false,
|
||
error: 'Data retention service unavailable',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /api/data-retention/health
|
||
* Get health status from data retention service
|
||
* RESTRICTED: Management users only
|
||
*/
|
||
router.get('/health', async (req, res) => {
|
||
try {
|
||
// Add security headers
|
||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||
res.setHeader('X-Frame-Options', 'DENY');
|
||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||
|
||
const response = await makeRequest('/health');
|
||
|
||
if (response.status === 200) {
|
||
console.log(`✅ Data retention health checked by ${req.managementUser.username}`);
|
||
|
||
res.json({
|
||
success: true,
|
||
data: response.data,
|
||
timestamp: new Date().toISOString(),
|
||
accessedBy: {
|
||
username: req.managementUser.username,
|
||
role: req.managementUser.role
|
||
}
|
||
});
|
||
} else {
|
||
res.status(response.status).json({
|
||
success: false,
|
||
error: 'Failed to fetch health from data retention service'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ Data retention health error for ${req.managementUser.username}:`, error);
|
||
res.status(503).json({
|
||
success: false,
|
||
error: 'Data retention service unavailable',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /api/data-retention/stats
|
||
* Get basic statistics from data retention service
|
||
* RESTRICTED: Management users only
|
||
*/
|
||
router.get('/stats', async (req, res) => {
|
||
try {
|
||
// Add security headers
|
||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||
res.setHeader('X-Frame-Options', 'DENY');
|
||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||
|
||
const response = await makeRequest('/stats');
|
||
|
||
if (response.status === 200) {
|
||
console.log(`✅ Data retention stats accessed by ${req.managementUser.username}`);
|
||
|
||
res.json({
|
||
success: true,
|
||
data: response.data,
|
||
timestamp: new Date().toISOString(),
|
||
accessedBy: {
|
||
username: req.managementUser.username,
|
||
role: req.managementUser.role
|
||
}
|
||
});
|
||
} else {
|
||
res.status(response.status).json({
|
||
success: false,
|
||
error: 'Failed to fetch stats from data retention service'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error(`❌ Data retention stats error for ${req.managementUser.username}:`, error);
|
||
res.status(503).json({
|
||
success: false,
|
||
error: 'Data retention service unavailable',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* GET /api/data-retention/status
|
||
* Get combined status including service connectivity
|
||
* RESTRICTED: Management users only
|
||
*/
|
||
router.get('/status', async (req, res) => {
|
||
try {
|
||
// Add security headers
|
||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||
res.setHeader('X-Frame-Options', 'DENY');
|
||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||
|
||
const [healthResponse, metricsResponse] = await Promise.allSettled([
|
||
makeRequest('/health'),
|
||
makeRequest('/metrics')
|
||
]);
|
||
|
||
const result = {
|
||
success: true,
|
||
service: {
|
||
name: 'data-retention-service',
|
||
connected: false,
|
||
error: null
|
||
},
|
||
health: null,
|
||
metrics: null,
|
||
timestamp: new Date().toISOString(),
|
||
accessedBy: {
|
||
username: req.managementUser.username,
|
||
role: req.managementUser.role
|
||
}
|
||
};
|
||
|
||
if (healthResponse.status === 'fulfilled' && healthResponse.value.status === 200) {
|
||
result.service.connected = true;
|
||
result.health = healthResponse.value.data;
|
||
} else {
|
||
result.service.error = healthResponse.reason?.message || 'Health check failed';
|
||
}
|
||
|
||
if (metricsResponse.status === 'fulfilled' && metricsResponse.value.status === 200) {
|
||
result.metrics = metricsResponse.value.data;
|
||
}
|
||
|
||
console.log(`✅ Data retention status accessed by ${req.managementUser.username}`);
|
||
res.json(result);
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Data retention status error for ${req.managementUser.username}:`, error);
|
||
res.status(503).json({
|
||
success: false,
|
||
error: 'Failed to get data retention service status',
|
||
details: error.message
|
||
});
|
||
}
|
||
});
|
||
|
||
module.exports = router; |