- 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.
315 lines
11 KiB
PHP
315 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Auth\Auth;
|
|
use App\Framework\Auth\RouteAuthorizationService;
|
|
use App\Framework\Auth\ValueObjects\NamespaceAccessPolicy;
|
|
use App\Framework\Config\TypedConfiguration;
|
|
use App\Framework\Http\IpAddress;
|
|
use App\Framework\Http\Request;
|
|
use App\Framework\Router\Exception\RouteNotFound;
|
|
use App\Framework\Router\RouteContext;
|
|
|
|
describe('RouteAuthorizationService', function () {
|
|
beforeEach(function () {
|
|
// Create stub TypedConfiguration
|
|
$this->config = createStubConfigForUnitTest(debug: false);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
describe('namespace-based access blocking', function () {
|
|
it('blocks all controllers when namespace is blocked', function () {
|
|
$namespaceConfig = [
|
|
'App\Application\Admin\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blocked(),
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
$request = createMockRequest('127.0.0.1');
|
|
$routeContext = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard'
|
|
);
|
|
|
|
expect(fn () => $service->authorize($request, $routeContext))
|
|
->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('allows controllers in allowlist when namespace is blocked', function () {
|
|
$namespaceConfig = [
|
|
'App\Application\Admin\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
|
'App\Application\Admin\LoginController'
|
|
),
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
$request = createMockRequest('127.0.0.1');
|
|
|
|
// LoginController should be allowed
|
|
$loginRoute = createMockRouteContext(
|
|
'App\Application\Admin\LoginController',
|
|
'/admin/login'
|
|
);
|
|
|
|
expect(fn () => $service->authorize($request, $loginRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
|
|
// Dashboard should be blocked
|
|
$dashboardRoute = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard'
|
|
);
|
|
|
|
expect(fn () => $service->authorize($request, $dashboardRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('allows all controllers when no access policy is set', function () {
|
|
$namespaceConfig = [
|
|
'App\Application\Admin\*' => [
|
|
'visibility' => 'public',
|
|
// No access_policy - should not block
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
$request = createMockRequest('127.0.0.1');
|
|
$routeContext = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard'
|
|
);
|
|
|
|
expect(fn () => $service->authorize($request, $routeContext))
|
|
->not->toThrow(RouteNotFound::class);
|
|
});
|
|
});
|
|
|
|
describe('namespace pattern matching', function () {
|
|
it('matches wildcard patterns', function () {
|
|
$namespaceConfig = [
|
|
'App\Application\Admin\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blocked(),
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
$request = createMockRequest('127.0.0.1');
|
|
|
|
// Should match App\Application\Admin\Dashboard
|
|
$route1 = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard'
|
|
);
|
|
expect(fn () => $service->authorize($request, $route1))
|
|
->toThrow(RouteNotFound::class);
|
|
|
|
// Should match App\Application\Admin\Users\UserController
|
|
$route2 = createMockRouteContext(
|
|
'App\Application\Admin\Users\UserController',
|
|
'/admin/users'
|
|
);
|
|
expect(fn () => $service->authorize($request, $route2))
|
|
->toThrow(RouteNotFound::class);
|
|
|
|
// Should NOT match App\Application\Api\UserController
|
|
$route3 = createMockRouteContext(
|
|
'App\Application\Api\UserController',
|
|
'/api/users'
|
|
);
|
|
expect(fn () => $service->authorize($request, $route3))
|
|
->not->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('matches exact namespaces', function () {
|
|
$namespaceConfig = [
|
|
'App\Application\Admin' => [
|
|
'access_policy' => NamespaceAccessPolicy::blocked(),
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
$request = createMockRequest('127.0.0.1');
|
|
|
|
// Should match exact namespace
|
|
$route1 = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard'
|
|
);
|
|
expect(fn () => $service->authorize($request, $route1))
|
|
->toThrow(RouteNotFound::class);
|
|
});
|
|
});
|
|
|
|
describe('multiple namespace configurations', function () {
|
|
it('applies first matching namespace config', function () {
|
|
$namespaceConfig = [
|
|
'App\Application\Admin\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blocked(),
|
|
],
|
|
'App\Application\Api\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blockedExcept(
|
|
'App\Application\Api\HealthController'
|
|
),
|
|
],
|
|
];
|
|
|
|
$service = new RouteAuthorizationService($this->config, $namespaceConfig);
|
|
|
|
$request = createMockRequest('127.0.0.1');
|
|
|
|
// Admin should be blocked
|
|
$adminRoute = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard'
|
|
);
|
|
expect(fn () => $service->authorize($request, $adminRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
|
|
// API Health should be allowed
|
|
$healthRoute = createMockRouteContext(
|
|
'App\Application\Api\HealthController',
|
|
'/api/health'
|
|
);
|
|
expect(fn () => $service->authorize($request, $healthRoute))
|
|
->not->toThrow(RouteNotFound::class);
|
|
|
|
// Other API should be blocked
|
|
$apiRoute = createMockRouteContext(
|
|
'App\Application\Api\UsersController',
|
|
'/api/users'
|
|
);
|
|
expect(fn () => $service->authorize($request, $apiRoute))
|
|
->toThrow(RouteNotFound::class);
|
|
});
|
|
});
|
|
|
|
describe('legacy Auth attribute', function () {
|
|
it('blocks non-wireguard IPs when Auth attribute is present', function () {
|
|
$service = new RouteAuthorizationService($this->config, []);
|
|
|
|
$request = createMockRequest('192.168.1.100');
|
|
$routeContext = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard',
|
|
attributes: [Auth::class]
|
|
);
|
|
|
|
expect(fn () => $service->authorize($request, $routeContext))
|
|
->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('allows wireguard IP when Auth attribute is present', function () {
|
|
$service = new RouteAuthorizationService($this->config, []);
|
|
|
|
$request = createMockRequest('172.20.0.1');
|
|
$routeContext = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard',
|
|
attributes: [Auth::class]
|
|
);
|
|
|
|
expect(fn () => $service->authorize($request, $routeContext))
|
|
->not->toThrow(RouteNotFound::class);
|
|
});
|
|
|
|
it('skips Auth check in debug mode', function () {
|
|
$debugConfig = createStubConfigForUnitTest(debug: true);
|
|
|
|
$service = new RouteAuthorizationService($debugConfig, []);
|
|
|
|
$request = createMockRequest('192.168.1.100');
|
|
$routeContext = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard',
|
|
attributes: [Auth::class]
|
|
);
|
|
|
|
// Should not throw in debug mode
|
|
expect(fn () => $service->authorize($request, $routeContext))
|
|
->not->toThrow(RouteNotFound::class);
|
|
});
|
|
});
|
|
|
|
describe('factory method', function () {
|
|
it('creates service with namespace config', function () {
|
|
$namespaceConfig = [
|
|
'App\Application\Admin\*' => [
|
|
'access_policy' => NamespaceAccessPolicy::blocked(),
|
|
],
|
|
];
|
|
|
|
$service = RouteAuthorizationService::withNamespaceConfig(
|
|
$this->config,
|
|
$namespaceConfig
|
|
);
|
|
|
|
expect($service)->toBeInstanceOf(RouteAuthorizationService::class);
|
|
|
|
$request = createMockRequest('127.0.0.1');
|
|
$routeContext = createMockRouteContext(
|
|
'App\Application\Admin\Dashboard',
|
|
'/admin/dashboard'
|
|
);
|
|
|
|
expect(fn () => $service->authorize($request, $routeContext))
|
|
->toThrow(RouteNotFound::class);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Helper functions for test setup
|
|
|
|
function createStubConfigForUnitTest(bool $debug = false): TypedConfiguration
|
|
{
|
|
return new class ($debug) extends TypedConfiguration {
|
|
public function __construct(bool $debug)
|
|
{
|
|
$this->app = (object)['debug' => $debug];
|
|
}
|
|
};
|
|
}
|
|
|
|
function createMockRequest(string $ipAddress): Request
|
|
{
|
|
$request = Mockery::mock(Request::class);
|
|
$server = Mockery::mock('server');
|
|
$server->shouldReceive('getClientIp')
|
|
->andReturn(IpAddress::fromString($ipAddress));
|
|
$request->server = $server;
|
|
|
|
return $request;
|
|
}
|
|
|
|
function createMockRouteContext(
|
|
string $controllerClass,
|
|
string $path,
|
|
array $attributes = []
|
|
): RouteContext {
|
|
$route = (object)[
|
|
'controller' => $controllerClass,
|
|
'action' => 'index',
|
|
'attributes' => $attributes,
|
|
];
|
|
|
|
$match = (object)['route' => $route];
|
|
|
|
$routeContext = Mockery::mock(RouteContext::class);
|
|
$routeContext->match = $match;
|
|
$routeContext->path = $path;
|
|
$routeContext->shouldReceive('isSuccess')->andReturn(true);
|
|
|
|
return $routeContext;
|
|
}
|