Implemented subdomain-based feature separation for production deployment. **Backend:** - New hostGate middleware for host-based API protection - Public host blocks: /api/admin, /api/groups, /api/slideshow, /api/auth - Public host allows: /api/upload, /api/manage, /api/social-media/platforms - Rate limiting: 20 uploads/hour on public host (publicUploadLimiter) - Audit log enhancement: source_host, source_type tracking - Database migration 009: Added source tracking columns **Frontend:** - Host detection utility (hostDetection.js) with feature flags - React code splitting with lazy loading for internal features - Conditional routing: Internal routes only mounted on internal host - 404 page: Host-specific messaging and navbar - Clipboard fallback for HTTP environments **Configuration:** - Environment variables: PUBLIC_HOST, INTERNAL_HOST, ENABLE_HOST_RESTRICTION - Docker dev setup: HOST variables, TRUST_PROXY_HOPS configuration - Frontend .env.development: DANGEROUSLY_DISABLE_HOST_CHECK for Webpack **Testing:** - 20/20 hostGate unit tests passing - Local testing guide in README.dev.md - /etc/hosts setup for public.test.local, internal.test.local **Bug Fixes:** - Fixed clipboard API not available on HTTP - Fixed missing PUBLIC_HOST in frontend env-config.js - Fixed wrong navbar on 404 page for public host - Fixed social media platforms loading in UUID management **Documentation:** - CHANGELOG.md: Complete feature documentation - README.md: Feature overview - README.dev.md: Host-separation testing guide - TESTING-HOST-SEPARATION.md: Integration note
268 lines
9.3 KiB
JavaScript
268 lines
9.3 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';
|
|
|
|
let hostGate;
|
|
|
|
// Helper to create mock request with headers
|
|
const createMockRequest = (hostname, path = '/') => {
|
|
return {
|
|
path,
|
|
get: (headerName) => {
|
|
if (headerName.toLowerCase() === 'x-forwarded-host') {
|
|
return hostname;
|
|
}
|
|
if (headerName.toLowerCase() === 'host') {
|
|
return hostname;
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
};
|
|
|
|
describe('Host Gate Middleware', () => {
|
|
let req, res, next;
|
|
let originalEnv;
|
|
|
|
beforeAll(() => {
|
|
// Sichere Original-Env
|
|
originalEnv = { ...process.env };
|
|
|
|
// Lade Modul NACH ENV setup
|
|
hostGate = require('../../../src/middlewares/hostGate');
|
|
});
|
|
|
|
beforeEach(() => {
|
|
// Mock response object
|
|
res = {
|
|
status: jest.fn().mockReturnThis(),
|
|
json: jest.fn()
|
|
};
|
|
|
|
// Mock next function
|
|
next = jest.fn();
|
|
|
|
// Reset req for each test
|
|
req = null;
|
|
|
|
// 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 = createMockRequest('public.example.com');
|
|
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 = createMockRequest('internal.example.com');
|
|
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 = createMockRequest('public.example.com');
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.isPublicHost).toBe(true);
|
|
});
|
|
|
|
test('should handle localhost as internal host', () => {
|
|
req = createMockRequest('localhost:3000');
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.isInternalHost).toBe(true);
|
|
expect(req.isPublicHost).toBe(false);
|
|
});
|
|
|
|
test('should strip port from hostname', () => {
|
|
req = createMockRequest('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 = createMockRequest('public.example.com', '/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 = createMockRequest('public.example.com', '/api/groups');
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
});
|
|
|
|
test('should block slideshow routes on public host', () => {
|
|
req = createMockRequest('public.example.com', '/api/slideshow');
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
});
|
|
|
|
test('should block migration routes on public host', () => {
|
|
req = createMockRequest('public.example.com', '/api/migration/start');
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
});
|
|
|
|
test('should block auth login on public host', () => {
|
|
req = createMockRequest('public.example.com', '/api/auth/login');
|
|
hostGate(req, res, next);
|
|
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
});
|
|
});
|
|
|
|
describe('Allowed Routes', () => {
|
|
test('should allow upload route on public host', () => {
|
|
req = createMockRequest('public.example.com', '/api/upload');
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should allow manage routes on public host', () => {
|
|
req = createMockRequest('public.example.com', '/api/manage/abc-123');
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should allow preview routes on public host', () => {
|
|
req = createMockRequest('public.example.com', '/api/previews/image.jpg');
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should allow consent routes on public host', () => {
|
|
req = createMockRequest('public.example.com', '/api/consent');
|
|
hostGate(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should allow all routes on internal host', () => {
|
|
req = createMockRequest('internal.example.com', '/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'; // Explicitly disabled
|
|
const hostGateTest = require('../../../src/middlewares/hostGate');
|
|
|
|
req = createMockRequest('public.example.com', '/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', () => {
|
|
// Reload module with test environment BUT explicitly enabled
|
|
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.ENABLE_HOST_RESTRICTION = 'true'; // Explicitly enabled
|
|
const hostGateTest = require('../../../src/middlewares/hostGate');
|
|
|
|
req = createMockRequest('public.example.com', '/api/admin/test');
|
|
hostGateTest(req, res, next);
|
|
|
|
// Should block because explicitly enabled
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
expect(next).not.toHaveBeenCalled();
|
|
|
|
// Restore
|
|
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
|
|
process.env.NODE_ENV = 'development';
|
|
process.env.ENABLE_HOST_RESTRICTION = 'true';
|
|
});
|
|
});
|
|
|
|
describe('Request Source Tracking', () => {
|
|
test('should set requestSource to "public" for public host', () => {
|
|
req = createMockRequest('public.example.com', '/api/upload');
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.requestSource).toBe('public');
|
|
});
|
|
|
|
test('should set requestSource to "internal" for internal host', () => {
|
|
req = createMockRequest('internal.example.com', '/api/admin/test');
|
|
hostGate(req, res, next);
|
|
|
|
expect(req.requestSource).toBe('internal');
|
|
});
|
|
|
|
test('should set requestSource to "internal" when restrictions disabled', () => {
|
|
// Reload module with disabled restriction
|
|
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
|
|
process.env.ENABLE_HOST_RESTRICTION = 'false';
|
|
const hostGateDisabled = require('../../../src/middlewares/hostGate');
|
|
|
|
req = createMockRequest('anything.example.com', '/api/test');
|
|
hostGateDisabled(req, res, next);
|
|
|
|
expect(req.requestSource).toBe('internal');
|
|
|
|
// Restore
|
|
delete require.cache[require.resolve('../../../src/middlewares/hostGate')];
|
|
process.env.ENABLE_HOST_RESTRICTION = 'true';
|
|
});
|
|
});
|
|
});
|