Backend: - Add hostGate middleware for host-based API protection - Extend rate limiter with publicUploadLimiter (20/hour) - Add source_host and source_type to audit logs - Database migration for audit log source tracking - Unit tests for hostGate middleware (10/20 passing) Frontend: - Add hostDetection utility for runtime host detection - Implement React code splitting with lazy loading - Update App.js with ProtectedRoute component - Customize 404 page for public vs internal hosts - Update env-config.js for host configuration Docker: - Add environment variables to prod/dev docker-compose - Configure ENABLE_HOST_RESTRICTION flags - Set PUBLIC_HOST and INTERNAL_HOST variables Infrastructure: - Prepared for nginx-proxy-manager setup - Trust proxy configuration (TRUST_PROXY_HOPS=1) Note: Some unit tests still need adjustment for ENV handling
277 lines
8.7 KiB
JavaScript
277 lines
8.7 KiB
JavaScript
/**
|
|
* Unit Tests für hostGate Middleware
|
|
* Testet Host-basierte Zugriffskontrolle
|
|
*/
|
|
|
|
// Setup ENV VOR dem Require
|
|
process.env.ENABLE_HOST_RESTRICTION = 'true';
|
|
process.env.PUBLIC_HOST = 'public.example.com';
|
|
process.env.INTERNAL_HOST = 'internal.example.com';
|
|
process.env.NODE_ENV = 'development';
|
|
|
|
const hostGate = require('../../../src/middlewares/hostGate');
|
|
|
|
describe('Host Gate Middleware', () => {
|
|
let req, res, next;
|
|
let originalEnv;
|
|
|
|
beforeAll(() => {
|
|
// Sichere Original-Env
|
|
originalEnv = { ...process.env };
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// Mock Request
|
|
req = {
|
|
get: jest.fn(),
|
|
path: '/api/admin/test'
|
|
};
|
|
|
|
// Mock Response
|
|
res = {
|
|
status: jest.fn().mockReturnThis(),
|
|
json: jest.fn()
|
|
};
|
|
|
|
// Mock Next
|
|
next = jest.fn();
|
|
|
|
// Setup Environment
|
|
process.env.ENABLE_HOST_RESTRICTION = 'true';
|
|
process.env.PUBLIC_HOST = 'public.example.com';
|
|
process.env.INTERNAL_HOST = 'internal.example.com';
|
|
process.env.NODE_ENV = 'development'; // NOT 'test' to enable restrictions
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
afterAll(() => {
|
|
// Restore Original-Env
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
describe('Host Detection', () => {
|
|
test('should detect public host from X-Forwarded-Host header', () => {
|
|
req.get.mockImplementation((header) => {
|
|
if (header === 'x-forwarded-host') return 'public.example.com';
|
|
return null;
|
|
});
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.isPublicHost).toBe(true);
|
|
expect(req.isInternalHost).toBe(false);
|
|
expect(req.requestSource).toBe('public');
|
|
});
|
|
|
|
test('should detect internal host from X-Forwarded-Host header', () => {
|
|
req.get.mockImplementation((header) => {
|
|
if (header === 'x-forwarded-host') return 'internal.example.com';
|
|
return null;
|
|
});
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.isPublicHost).toBe(false);
|
|
expect(req.isInternalHost).toBe(true);
|
|
expect(req.requestSource).toBe('internal');
|
|
});
|
|
|
|
test('should fallback to Host header if X-Forwarded-Host not present', () => {
|
|
req.get.mockImplementation((header) => {
|
|
if (header === 'x-forwarded-host') return null;
|
|
if (header === 'host') return 'public.example.com';
|
|
return null;
|
|
});
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.isPublicHost).toBe(true);
|
|
});
|
|
|
|
test('should handle localhost as internal host', () => {
|
|
req.get.mockImplementation((header) => {
|
|
if (header === 'x-forwarded-host') return null;
|
|
if (header === 'host') return 'localhost:3000';
|
|
return null;
|
|
});
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.isInternalHost).toBe(true);
|
|
expect(req.isPublicHost).toBe(false);
|
|
});
|
|
|
|
test('should strip port from hostname', () => {
|
|
req.get.mockReturnValue('public.example.com:8080');
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.isPublicHost).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('Route Protection', () => {
|
|
test('should block admin routes on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/admin/deletion-log';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Not available on public host',
|
|
message: 'This endpoint is only available on the internal network'
|
|
});
|
|
expect(next).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should block groups routes on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/groups';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
});
|
|
|
|
test('should block slideshow routes on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/slideshow';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
});
|
|
|
|
test('should block migration routes on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/migration/start';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
});
|
|
|
|
test('should block auth login on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/auth/login';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
});
|
|
});
|
|
|
|
describe('Allowed Routes', () => {
|
|
test('should allow upload route on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/upload';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should allow manage routes on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/manage/abc-123';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should allow preview routes on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/previews/image.jpg';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should allow consent routes on public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/consent';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should allow all routes on internal host', () => {
|
|
req.get.mockReturnValue('internal.example.com');
|
|
req.path = '/api/admin/deletion-log';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Feature Flags', () => {
|
|
test('should bypass restriction when NODE_ENV is test and not explicitly enabled', () => {
|
|
// Reload module with test environment
|
|
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.ENABLE_HOST_RESTRICTION = 'false'; // Not explicitly enabled
|
|
const hostGateTest = require('../../../src/middlewares/hostGate');
|
|
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/admin/test';
|
|
|
|
hostGateTest(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
expect(req.isInternalHost).toBe(true);
|
|
|
|
// Restore
|
|
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
|
|
process.env.NODE_ENV = 'development';
|
|
process.env.ENABLE_HOST_RESTRICTION = 'true';
|
|
});
|
|
|
|
test('should work in test environment when explicitly enabled', () => {
|
|
// Already set up correctly
|
|
process.env.NODE_ENV = 'development';
|
|
expect(req.isInternalHost).toBeUndefined(); // Not processed yet, just checking setup
|
|
});
|
|
});
|
|
|
|
describe('Request Source Tracking', () => {
|
|
test('should set requestSource to "public" for public host', () => {
|
|
req.get.mockReturnValue('public.example.com');
|
|
req.path = '/api/upload';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.requestSource).toBe('public');
|
|
});
|
|
|
|
test('should set requestSource to "internal" for internal host', () => {
|
|
req.get.mockReturnValue('internal.example.com');
|
|
req.path = '/api/admin/test';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.requestSource).toBe('internal');
|
|
});
|
|
|
|
test('should set requestSource to "internal" when restrictions disabled', () => {
|
|
process.env.ENABLE_HOST_RESTRICTION = 'false';
|
|
req.get.mockReturnValue('anything.example.com');
|
|
req.path = '/api/test';
|
|
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.requestSource).toBe('internal');
|
|
});
|
|
});
|
|
});
|