- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
248 lines
9.3 KiB
PHP
248 lines
9.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Auth\RouteAuthorizationService;
|
|
use App\Framework\Auth\ValueObjects\NamespaceAccessPolicy;
|
|
use App\Framework\Config\TypedConfiguration;
|
|
use App\Framework\DI\DefaultContainer;
|
|
use App\Framework\Http\HttpRequest;
|
|
use App\Framework\Http\IpAddress;
|
|
use App\Framework\Http\Method;
|
|
use App\Framework\Router\Exception\RouteNotFound;
|
|
use App\Framework\Router\RouteContext;
|
|
|
|
describe('Route Authorization Integration', function () {
|
|
beforeEach(function () {
|
|
$this->container = new DefaultContainer();
|
|
|
|
// Setup stub config
|
|
$this->config = createStubConfig(debug: false);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
it('integrates namespace blocking with IP restrictions', function () {
|
|
// Real-world scenario: Admin area with IP restrictions AND namespace blocking
|
|
$namespaceConfig = [
|
|
'App\Application\Admin\*' => [
|
|
'visibility' => 'admin', // IP-restricted to admin IPs
|
|
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
|
'App\Application\Admin\LoginController',
|
|
'App\Application\Admin\HealthController'
|
|
),
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
// Test 1: Login should be accessible from any IP (in allowlist)
|
|
$loginRequest = createHttpRequest('192.168.1.100', '/admin/login');
|
|
$loginRoute = createRouteContext('App\Application\Admin\LoginController', '/admin/login');
|
|
|
|
expect(fn () => $service->authorize($loginRequest, $loginRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
|
|
// Test 2: Dashboard blocked (not in allowlist)
|
|
$dashboardRequest = createHttpRequest('192.168.1.100', '/admin/dashboard');
|
|
$dashboardRoute = createRouteContext('App\Application\Admin\Dashboard', '/admin/dashboard');
|
|
|
|
expect(fn () => $service->authorize($dashboardRequest, $dashboardRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
|
|
// Test 3: Health controller allowed (in allowlist)
|
|
$healthRequest = createHttpRequest('10.0.0.5', '/admin/health');
|
|
$healthRoute = createRouteContext('App\Application\Admin\HealthController', '/admin/health');
|
|
|
|
expect(fn () => $service->authorize($healthRequest, $healthRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('handles multiple namespace configurations independently', function () {
|
|
$namespaceConfig = [
|
|
// Admin: Completely blocked
|
|
'App\Application\Admin\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blocked(),
|
|
],
|
|
// API: Blocked except health endpoint
|
|
'App\Application\Api\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
|
'App\Application\Api\HealthController'
|
|
),
|
|
],
|
|
// Web: No restrictions
|
|
'App\Application\Web\*' => [
|
|
'visibility' => 'public',
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
$request = createHttpRequest('127.0.0.1', '/test');
|
|
|
|
// Admin: All blocked
|
|
$adminRoute = createRouteContext('App\Application\Admin\Dashboard', '/admin/dashboard');
|
|
expect(fn () => $service->authorize($request, $adminRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
|
|
// API: Health allowed
|
|
$apiHealthRoute = createRouteContext('App\Application\Api\HealthController', '/api/health');
|
|
expect(fn () => $service->authorize($request, $apiHealthRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
|
|
// API: Users blocked
|
|
$apiUsersRoute = createRouteContext('App\Application\Api\UsersController', '/api/users');
|
|
expect(fn () => $service->authorize($request, $apiUsersRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
|
|
// Web: Allowed
|
|
$webRoute = createRouteContext('App\Application\Web\HomeController', '/');
|
|
expect(fn () => $service->authorize($request, $webRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('production-like configuration scenario', function () {
|
|
// Realistic production setup
|
|
$namespaceConfig = [
|
|
// Internal tools - completely locked down
|
|
'App\Application\Internal\*' => [
|
|
'visibility' => 'admin',
|
|
'access_policy' => NamespaceAccessPolicy::blocked(),
|
|
],
|
|
|
|
// Admin - IP restricted, only public endpoints accessible
|
|
'App\Application\Admin\*' => [
|
|
'visibility' => 'admin',
|
|
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
|
'App\Application\Admin\LoginController'
|
|
),
|
|
],
|
|
|
|
// API - public but monitored
|
|
'App\Application\Api\*' => [
|
|
'visibility' => 'public',
|
|
// No access_policy - all API routes accessible
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
// Public user accessing different areas
|
|
$publicRequest = createHttpRequest('93.184.216.34', '/test');
|
|
|
|
// Internal: Blocked (even from public IP, namespace blocked + IP restricted)
|
|
$internalRoute = createRouteContext('App\Application\Internal\ToolsController', '/internal/tools');
|
|
expect(fn () => $service->authorize($publicRequest, $internalRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
|
|
// Admin Login: Allowed (in allowlist, IP restriction doesn't apply to allowlist)
|
|
$adminLoginRoute = createRouteContext('App\Application\Admin\LoginController', '/admin/login');
|
|
expect(fn () => $service->authorize($publicRequest, $adminLoginRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
|
|
// Admin Dashboard: Blocked (not in allowlist)
|
|
$adminDashboardRoute = createRouteContext('App\Application\Admin\Dashboard', '/admin/dashboard');
|
|
expect(fn () => $service->authorize($publicRequest, $adminDashboardRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
|
|
// API: Allowed (public visibility, no namespace blocking)
|
|
$apiRoute = createRouteContext('App\Application\Api\UsersController', '/api/users');
|
|
expect(fn () => $service->authorize($publicRequest, $apiRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('handles deeply nested namespaces correctly', function () {
|
|
$namespaceConfig = [
|
|
'App\Application\Admin\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
|
'App\Application\Admin\Auth\LoginController'
|
|
),
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
$request = createHttpRequest('127.0.0.1', '/test');
|
|
|
|
// Deeply nested allowed controller
|
|
$loginRoute = createRouteContext(
|
|
'App\Application\Admin\Auth\LoginController',
|
|
'/admin/auth/login'
|
|
);
|
|
expect(fn () => $service->authorize($request, $loginRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
|
|
// Deeply nested blocked controller
|
|
$userRoute = createRouteContext(
|
|
'App\Application\Admin\Users\Management\UserController',
|
|
'/admin/users/management'
|
|
);
|
|
expect(fn () => $service->authorize($request, $userRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('handles no namespace configuration gracefully', function () {
|
|
// Service with empty config should allow everything
|
|
$service = new RouteAuthorizationService($this->config, []);
|
|
|
|
$request = createHttpRequest('127.0.0.1', '/test');
|
|
|
|
$routes = [
|
|
createRouteContext('App\Application\Admin\Dashboard', '/admin/dashboard'),
|
|
createRouteContext('App\Application\Api\UsersController', '/api/users'),
|
|
createRouteContext('App\Application\Web\HomeController', '/'),
|
|
];
|
|
|
|
foreach ($routes as $route) {
|
|
expect(fn () => $service->authorize($request, $route))
|
|
->not->toThrow(RouteNotFound::class);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Helper functions
|
|
|
|
function createStubConfig(bool $debug = false): TypedConfiguration
|
|
{
|
|
return new class ($debug) extends TypedConfiguration {
|
|
public function __construct(bool $debug)
|
|
{
|
|
$this->app = (object)['debug' => $debug];
|
|
}
|
|
};
|
|
}
|
|
|
|
function createHttpRequest(string $ipAddress, string $path): HttpRequest
|
|
{
|
|
$server = Mockery::mock('server');
|
|
$server->shouldReceive('getClientIp')
|
|
->andReturn(IpAddress::fromString($ipAddress));
|
|
|
|
$request = Mockery::mock(HttpRequest::class);
|
|
$request->server = $server;
|
|
$request->path = $path;
|
|
$request->method = Method::GET;
|
|
|
|
return $request;
|
|
}
|
|
|
|
function createRouteContext(string $controllerClass, string $path): RouteContext
|
|
{
|
|
$route = (object)[
|
|
'controller' => $controllerClass,
|
|
'action' => 'index',
|
|
'attributes' => [],
|
|
];
|
|
|
|
$match = (object)['route' => $route];
|
|
|
|
$routeContext = Mockery::mock(RouteContext::class);
|
|
$routeContext->match = $match;
|
|
$routeContext->path = $path;
|
|
$routeContext->shouldReceive('isSuccess')->andReturn(true);
|
|
|
|
return $routeContext;
|
|
}
|