Fix jwt-token
This commit is contained in:
@@ -349,9 +349,10 @@ class DataRetentionService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, '0.0.0.0', () => {
|
||||||
console.log(`📊 Metrics server listening on port ${port}`);
|
console.log(`📊 Metrics server listening on internal port ${port}`);
|
||||||
console.log(`📊 Endpoints: /health, /metrics, /stats`);
|
console.log(`📊 Endpoints: /health, /metrics, /stats`);
|
||||||
|
console.log(`🔒 Access restricted to Docker internal network only`);
|
||||||
});
|
});
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
|
|||||||
@@ -191,8 +191,7 @@ services:
|
|||||||
NODE_ENV: ${NODE_ENV:-production}
|
NODE_ENV: ${NODE_ENV:-production}
|
||||||
IMMEDIATE_CLEANUP: ${IMMEDIATE_CLEANUP:-false}
|
IMMEDIATE_CLEANUP: ${IMMEDIATE_CLEANUP:-false}
|
||||||
METRICS_PORT: 3001
|
METRICS_PORT: 3001
|
||||||
ports:
|
# No external ports exposed - internal access only
|
||||||
- "3004:3001" # Expose metrics port
|
|
||||||
networks:
|
networks:
|
||||||
- drone-network
|
- drone-network
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -1,15 +1,141 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { ManagementUser } = require('../models');
|
||||||
|
const auditLogger = require('../utils/dataRetentionAuditLogger');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data Retention Metrics Proxy
|
* Data Retention Metrics Proxy
|
||||||
* Proxies requests to the data retention microservice
|
* 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_HOST = process.env.DATA_RETENTION_HOST || 'data-retention';
|
||||||
const DATA_RETENTION_PORT = process.env.DATA_RETENTION_PORT || 3001;
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IP restriction middleware (additional security layer)
|
||||||
|
* Only allow access from management container or specified IPs
|
||||||
|
*/
|
||||||
|
const requireManagementNetwork = async (req, res, next) => {
|
||||||
|
const clientIP = req.ip || req.connection.remoteAddress;
|
||||||
|
const forwardedFor = req.headers['x-forwarded-for'];
|
||||||
|
|
||||||
|
// Allow internal Docker network (management container)
|
||||||
|
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 management container or allowed 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) {
|
||||||
|
await auditLogger.logNetworkDenied(req, req.path);
|
||||||
|
console.warn(`🚫 Data retention access denied from IP: ${clientIP} (forwarded: ${forwardedFor})`);
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Network access denied',
|
||||||
|
message: 'Access to data retention metrics is restricted to management network'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make HTTP request to data retention service
|
* Make HTTP request to data retention service
|
||||||
*/
|
*/
|
||||||
@@ -53,19 +179,39 @@ function makeRequest(path) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply security middleware to all data retention routes
|
||||||
|
*/
|
||||||
|
router.use(requireManagementNetwork);
|
||||||
|
router.use(requireManagementAuth);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/data-retention/metrics
|
* GET /api/data-retention/metrics
|
||||||
* Get detailed metrics from data retention service
|
* Get detailed metrics from data retention service
|
||||||
|
* RESTRICTED: Management users only
|
||||||
*/
|
*/
|
||||||
router.get('/metrics', async (req, res) => {
|
router.get('/metrics', async (req, res) => {
|
||||||
try {
|
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');
|
const response = await makeRequest('/metrics');
|
||||||
|
|
||||||
if (response.status === 200) {
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
accessedBy: {
|
||||||
|
username: req.managementUser.username,
|
||||||
|
role: req.managementUser.role
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(response.status).json({
|
res.status(response.status).json({
|
||||||
@@ -74,7 +220,7 @@ router.get('/metrics', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Data retention metrics error:', error);
|
console.error(`❌ Data retention metrics error for ${req.managementUser.username}:`, error);
|
||||||
res.status(503).json({
|
res.status(503).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Data retention service unavailable',
|
error: 'Data retention service unavailable',
|
||||||
@@ -86,16 +232,28 @@ router.get('/metrics', async (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* GET /api/data-retention/health
|
* GET /api/data-retention/health
|
||||||
* Get health status from data retention service
|
* Get health status from data retention service
|
||||||
|
* RESTRICTED: Management users only
|
||||||
*/
|
*/
|
||||||
router.get('/health', async (req, res) => {
|
router.get('/health', async (req, res) => {
|
||||||
try {
|
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');
|
const response = await makeRequest('/health');
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
|
console.log(`✅ Data retention health checked by ${req.managementUser.username}`);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
accessedBy: {
|
||||||
|
username: req.managementUser.username,
|
||||||
|
role: req.managementUser.role
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(response.status).json({
|
res.status(response.status).json({
|
||||||
@@ -104,7 +262,7 @@ router.get('/health', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Data retention health error:', error);
|
console.error(`❌ Data retention health error for ${req.managementUser.username}:`, error);
|
||||||
res.status(503).json({
|
res.status(503).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Data retention service unavailable',
|
error: 'Data retention service unavailable',
|
||||||
@@ -116,16 +274,28 @@ router.get('/health', async (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* GET /api/data-retention/stats
|
* GET /api/data-retention/stats
|
||||||
* Get basic statistics from data retention service
|
* Get basic statistics from data retention service
|
||||||
|
* RESTRICTED: Management users only
|
||||||
*/
|
*/
|
||||||
router.get('/stats', async (req, res) => {
|
router.get('/stats', async (req, res) => {
|
||||||
try {
|
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');
|
const response = await makeRequest('/stats');
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
|
console.log(`✅ Data retention stats accessed by ${req.managementUser.username}`);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
accessedBy: {
|
||||||
|
username: req.managementUser.username,
|
||||||
|
role: req.managementUser.role
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(response.status).json({
|
res.status(response.status).json({
|
||||||
@@ -134,7 +304,7 @@ router.get('/stats', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Data retention stats error:', error);
|
console.error(`❌ Data retention stats error for ${req.managementUser.username}:`, error);
|
||||||
res.status(503).json({
|
res.status(503).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Data retention service unavailable',
|
error: 'Data retention service unavailable',
|
||||||
@@ -146,9 +316,15 @@ router.get('/stats', async (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* GET /api/data-retention/status
|
* GET /api/data-retention/status
|
||||||
* Get combined status including service connectivity
|
* Get combined status including service connectivity
|
||||||
|
* RESTRICTED: Management users only
|
||||||
*/
|
*/
|
||||||
router.get('/status', async (req, res) => {
|
router.get('/status', async (req, res) => {
|
||||||
try {
|
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([
|
const [healthResponse, metricsResponse] = await Promise.allSettled([
|
||||||
makeRequest('/health'),
|
makeRequest('/health'),
|
||||||
makeRequest('/metrics')
|
makeRequest('/metrics')
|
||||||
@@ -163,7 +339,11 @@ router.get('/status', async (req, res) => {
|
|||||||
},
|
},
|
||||||
health: null,
|
health: null,
|
||||||
metrics: null,
|
metrics: null,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString(),
|
||||||
|
accessedBy: {
|
||||||
|
username: req.managementUser.username,
|
||||||
|
role: req.managementUser.role
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (healthResponse.status === 'fulfilled' && healthResponse.value.status === 200) {
|
if (healthResponse.status === 'fulfilled' && healthResponse.value.status === 200) {
|
||||||
@@ -177,10 +357,11 @@ router.get('/status', async (req, res) => {
|
|||||||
result.metrics = metricsResponse.value.data;
|
result.metrics = metricsResponse.value.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Data retention status accessed by ${req.managementUser.username}`);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Data retention status error:', error);
|
console.error(`❌ Data retention status error for ${req.managementUser.username}:`, error);
|
||||||
res.status(503).json({
|
res.status(503).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to get data retention service status',
|
error: 'Failed to get data retention service status',
|
||||||
|
|||||||
107
server/utils/dataRetentionAuditLogger.js
Normal file
107
server/utils/dataRetentionAuditLogger.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* Security Audit Logger for Data Retention Access
|
||||||
|
* Logs all access attempts to data retention metrics
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class DataRetentionAuditLogger {
|
||||||
|
constructor() {
|
||||||
|
this.logDir = process.env.SECURITY_LOG_DIR || './logs';
|
||||||
|
this.logFile = path.join(this.logDir, 'data_retention_access.log');
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureLogDir() {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(this.logDir, { recursive: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create security log directory:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logAccess(event) {
|
||||||
|
try {
|
||||||
|
await this.ensureLogDir();
|
||||||
|
|
||||||
|
const logEntry = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
event: event.type,
|
||||||
|
user: {
|
||||||
|
id: event.user?.id,
|
||||||
|
username: event.user?.username,
|
||||||
|
role: event.user?.role
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
ip: event.ip,
|
||||||
|
userAgent: event.userAgent,
|
||||||
|
endpoint: event.endpoint,
|
||||||
|
method: event.method
|
||||||
|
},
|
||||||
|
result: event.result,
|
||||||
|
error: event.error
|
||||||
|
};
|
||||||
|
|
||||||
|
const logLine = JSON.stringify(logEntry) + '\n';
|
||||||
|
await fs.appendFile(this.logFile, logLine, 'utf8');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to write security log:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log successful access
|
||||||
|
async logSuccess(user, req, endpoint) {
|
||||||
|
await this.logAccess({
|
||||||
|
type: 'DATA_RETENTION_ACCESS_SUCCESS',
|
||||||
|
user,
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.headers['user-agent'],
|
||||||
|
endpoint,
|
||||||
|
method: req.method,
|
||||||
|
result: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log authentication failure
|
||||||
|
async logAuthFailure(req, endpoint, reason) {
|
||||||
|
await this.logAccess({
|
||||||
|
type: 'DATA_RETENTION_ACCESS_AUTH_FAILED',
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.headers['user-agent'],
|
||||||
|
endpoint,
|
||||||
|
method: req.method,
|
||||||
|
result: 'auth_failed',
|
||||||
|
error: reason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log permission denied
|
||||||
|
async logPermissionDenied(user, req, endpoint, reason) {
|
||||||
|
await this.logAccess({
|
||||||
|
type: 'DATA_RETENTION_ACCESS_PERMISSION_DENIED',
|
||||||
|
user,
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.headers['user-agent'],
|
||||||
|
endpoint,
|
||||||
|
method: req.method,
|
||||||
|
result: 'permission_denied',
|
||||||
|
error: reason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log network access denied
|
||||||
|
async logNetworkDenied(req, endpoint) {
|
||||||
|
await this.logAccess({
|
||||||
|
type: 'DATA_RETENTION_ACCESS_NETWORK_DENIED',
|
||||||
|
ip: req.ip,
|
||||||
|
userAgent: req.headers['user-agent'],
|
||||||
|
endpoint,
|
||||||
|
method: req.method,
|
||||||
|
result: 'network_denied',
|
||||||
|
error: 'Access from unauthorized network'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new DataRetentionAuditLogger();
|
||||||
Reference in New Issue
Block a user