feat(Production): Complete production deployment infrastructure

- 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.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,314 @@
<?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;
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
use App\Framework\Auth\ValueObjects\NamespaceAccessPolicy;
describe('NamespaceAccessPolicy', function () {
describe('blocked()', function () {
it('creates policy that blocks all controllers', function () {
$policy = NamespaceAccessPolicy::blocked();
expect($policy->isControllerBlocked('App\Application\Admin\Dashboard'))->toBeTrue();
expect($policy->isControllerBlocked('App\Application\Admin\UserController'))->toBeTrue();
expect($policy->hasRestrictions())->toBeTrue();
});
});
describe('blockedExcept()', function () {
it('blocks all except allowed controllers', function () {
$policy = NamespaceAccessPolicy::blockedExcept(
'App\Application\Admin\LoginController',
'App\Application\Admin\HealthController'
);
expect($policy->isControllerBlocked('App\Application\Admin\LoginController'))->toBeFalse();
expect($policy->isControllerBlocked('App\Application\Admin\HealthController'))->toBeFalse();
expect($policy->isControllerBlocked('App\Application\Admin\Dashboard'))->toBeTrue();
expect($policy->hasRestrictions())->toBeTrue();
});
it('handles empty allowlist', function () {
$policy = NamespaceAccessPolicy::blockedExcept();
expect($policy->isControllerBlocked('App\Application\Admin\Dashboard'))->toBeTrue();
});
});
describe('allowed()', function () {
it('allows all controllers', function () {
$policy = NamespaceAccessPolicy::allowed();
expect($policy->isControllerBlocked('App\Application\Admin\Dashboard'))->toBeFalse();
expect($policy->isControllerBlocked('App\Application\Admin\UserController'))->toBeFalse();
expect($policy->hasRestrictions())->toBeFalse();
});
});
describe('withAllowedControllers()', function () {
it('adds controllers to allowlist', function () {
$policy = NamespaceAccessPolicy::blocked();
$newPolicy = $policy->withAllowedControllers(
'App\Application\Admin\LoginController'
);
expect($newPolicy->isControllerBlocked('App\Application\Admin\LoginController'))->toBeFalse();
expect($newPolicy->isControllerBlocked('App\Application\Admin\Dashboard'))->toBeTrue();
});
it('preserves existing allowlist', function () {
$policy = NamespaceAccessPolicy::blockedExcept(
'App\Application\Admin\LoginController'
);
$newPolicy = $policy->withAllowedControllers(
'App\Application\Admin\HealthController'
);
expect($newPolicy->isControllerBlocked('App\Application\Admin\LoginController'))->toBeFalse();
expect($newPolicy->isControllerBlocked('App\Application\Admin\HealthController'))->toBeFalse();
expect($newPolicy->isControllerBlocked('App\Application\Admin\Dashboard'))->toBeTrue();
});
it('handles duplicate controllers', function () {
$policy = NamespaceAccessPolicy::blockedExcept(
'App\Application\Admin\LoginController'
);
$newPolicy = $policy->withAllowedControllers(
'App\Application\Admin\LoginController',
'App\Application\Admin\HealthController'
);
expect($newPolicy->isControllerBlocked('App\Application\Admin\LoginController'))->toBeFalse();
expect($newPolicy->isControllerBlocked('App\Application\Admin\HealthController'))->toBeFalse();
});
});
describe('immutability', function () {
it('does not modify original policy when adding controllers', function () {
$original = NamespaceAccessPolicy::blocked();
$modified = $original->withAllowedControllers(
'App\Application\Admin\LoginController'
);
expect($original->isControllerBlocked('App\Application\Admin\LoginController'))->toBeTrue();
expect($modified->isControllerBlocked('App\Application\Admin\LoginController'))->toBeFalse();
});
});
});