Files
michaelschiemer/docs/features/security/route-authorization.md
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

7.8 KiB

Route Authorization System

Dokumentation des namespace-basierten Route Authorization Systems.

Übersicht

Das Route Authorization System ermöglicht die Zugangskontrolle für Routes auf Basis von:

  1. Legacy #[Auth] Attribute - Backward compatibility
  2. Namespace-basierte Blockierung - Blockiere ganze Controller-Namespaces (z.B. App\Application\Admin\*)
  3. Namespace-basierte IP-Restrictions - IP-basierte Zugriffskontrolle per Namespace
  4. Route-spezifische #[IpAuth] Attribute - Feinkörnige IP-Kontrolle per Route

Architektur

RouteAuthorizationService
├── checkLegacyAuthAttribute()
├── checkNamespaceAccessPolicy()      [NEU]
├── checkNamespaceIpRestrictions()
└── checkRouteIpAuthAttribute()

Der Service wird in der RoutingMiddleware nach dem Routing aufgerufen, sodass die gematchte Route bekannt ist.

Konfiguration

Initializer-basierte Konfiguration

Location: src/Framework/Auth/RouteAuthorizationServiceInitializer.php

#[Initializer]
public function __invoke(Container $container): RouteAuthorizationService
{
    $namespaceConfig = [
        // Namespace Pattern => Configuration
        'App\Application\Admin\*' => [
            'visibility' => 'admin',  // IP-based restriction
            'access_policy' => NamespaceAccessPolicy::blocked()
        ],
    ];

    return new RouteAuthorizationService(
        config: $this->config,
        namespaceConfig: $namespaceConfig
    );
}

Namespace Patterns

Wildcard-basierte Patterns:

  • App\Application\Admin\* - Matched alle Admin-Controller
  • App\Application\Api\* - Matched alle API-Controller
  • App\Application\* - Matched alle Application-Controller

Exact Match:

  • App\Application\Admin\Dashboard - Nur exakt dieser Namespace

Use Cases

1. Admin-Bereich komplett blockieren

$namespaceConfig = [
    'App\Application\Admin\*' => [
        'access_policy' => NamespaceAccessPolicy::blocked()
    ],
];

Ergebnis: Alle Admin-Controller werfen RouteNotFound (404)

2. Admin-Bereich mit Allowlist

use App\Application\Admin\LoginController;
use App\Application\Admin\HealthCheckController;

$namespaceConfig = [
    'App\Application\Admin\*' => [
        'access_policy' => NamespaceAccessPolicy::blockedExcept(
            LoginController::class,
            HealthCheckController::class
        )
    ],
];

Ergebnis:

  • LoginController und HealthCheckController öffentlich erreichbar
  • Alle anderen Admin-Controller blockiert

3. Kombination: IP-Restriction + Namespace-Blocking

$namespaceConfig = [
    'App\Application\Admin\*' => [
        'visibility' => 'admin',  // Nur Admin-IPs erlaubt
        'access_policy' => NamespaceAccessPolicy::blockedExcept(
            LoginController::class  // Aber Login ist public
        )
    ],
];

Ergebnis:

  • LoginController - öffentlich erreichbar (trotz admin visibility)
  • 🔒 Alle anderen Admin-Controller - nur von Admin-IPs

4. API-Bereich mit IP-Restriction (ohne Blocking)

$namespaceConfig = [
    'App\Application\Api\*' => [
        'visibility' => 'local',  // Nur localhost/private IPs
        // Kein access_policy - keine Namespace-Blockierung
    ],
];

Ergebnis: API nur von localhost/private IPs erreichbar

5. Mehrere Namespace-Policies

$namespaceConfig = [
    // Admin komplett gesperrt
    'App\Application\Admin\*' => [
        'access_policy' => NamespaceAccessPolicy::blocked()
    ],

    // API nur von localhost
    'App\Application\Api\*' => [
        'visibility' => 'local'
    ],

    // Internal Tools nur admin IPs
    'App\Application\Internal\*' => [
        'visibility' => 'admin',
        'access_policy' => NamespaceAccessPolicy::blocked()
    ],
];

Value Objects

NamespaceAccessPolicy

// Alle Controller im Namespace blockieren
NamespaceAccessPolicy::blocked()

// Alle blockieren außer spezifische Controller
NamespaceAccessPolicy::blockedExcept(
    LoginController::class,
    HealthCheckController::class
)

// Alle erlauben (default)
NamespaceAccessPolicy::allowed()

// Prüfen ob Controller blockiert ist
$policy->isControllerBlocked(Dashboard::class)  // true/false

Visibility Modes (IP-Restrictions)

Predefined Modes:

  • public - Keine IP-Restrictions
  • admin - Nur Admin-IPs (WireGuard, etc.)
  • local - Nur localhost/127.0.0.1
  • development - Development-IPs
  • private - Alias für local
  • custom - Custom IP-Liste via Config

Execution Order

  1. Legacy Auth Attribute - Backward compatibility check
  2. Namespace Access Policy - Block/Allow basierend auf Controller-Klasse
  3. Namespace IP Restrictions - IP-basierte Zugriffskontrolle
  4. Route IP Auth Attribute - Feinkörnige Route-Level IP-Kontrolle

Alle Checks werfen RouteNotFound bei Failure (versteckt Route-Existenz).

Testing

Unit Test Beispiel

use App\Framework\Auth\RouteAuthorizationService;
use App\Framework\Auth\ValueObjects\NamespaceAccessPolicy;

it('blocks admin controllers except allowlist', function () {
    $config = ['App\Application\Admin\*' => [
        'access_policy' => NamespaceAccessPolicy::blockedExcept(
            LoginController::class
        )
    ]];

    $service = new RouteAuthorizationService(
        config: $this->config,
        namespaceConfig: $config
    );

    // Should throw RouteNotFound for Dashboard
    expect(fn() => $service->authorize($request, $dashboardRoute))
        ->toThrow(RouteNotFound::class);

    // Should allow LoginController
    expect(fn() => $service->authorize($request, $loginRoute))
        ->not->toThrow(RouteNotFound::class);
});

Best Practices

1. Namespace-Blocking für Production

  • Blockiere Admin/Internal-Bereiche in Production
  • Nutze Allowlist nur für wirklich öffentliche Endpoints (Login, Health)

2. IP-Restrictions für Sensitive Bereiche

  • Kombiniere Namespace-Blocking mit IP-Restrictions
  • Nutze visibility: 'admin' für maximale Sicherheit

3. Graceful Error Handling

  • Alle Checks werfen RouteNotFound (404)
  • Versteckt Route-Existenz vor Angreifern
  • Keine Information Leakage

4. Konfiguration via Initializer

  • Zentrale Konfiguration in RouteAuthorizationServiceInitializer
  • Environment-spezifische Configs möglich (dev vs. production)
  • Type-safe via Value Objects

Migration Guide

Von altem System (RoutingMiddleware::withNamespaceConfig)

Alt:

RoutingMiddleware::withNamespaceConfig(
    $router, $dispatcher, $config, $performance, $container,
    namespaceConfig: [
        'App\Application\Admin\*' => ['visibility' => 'admin']
    ]
);

Neu:

// In RouteAuthorizationServiceInitializer
$namespaceConfig = [
    'App\Application\Admin\*' => [
        'visibility' => 'admin',
        'access_policy' => NamespaceAccessPolicy::blocked()  // NEU
    ]
];

Troubleshooting

Problem: Route wirft 404 obwohl sie erreichbar sein sollte

Debugging:

  1. Prüfe RouteAuthorizationServiceInitializer Config
  2. Check ob Controller-Namespace in namespaceConfig matched
  3. Prüfe access_policy - ist Controller in Allowlist?
  4. Check IP-Restrictions (visibility)

Problem: Allowlist funktioniert nicht

Ursache: Controller-Klasse exakt mit ::class angeben

// ❌ Falsch
NamespaceAccessPolicy::blockedExcept('LoginController')

// ✅ Korrekt
NamespaceAccessPolicy::blockedExcept(
    \App\Application\Admin\LoginController::class
)

Framework Integration

  • Automatic Discovery: Service wird via #[Initializer] automatisch registriert
  • DI Container: Alle Dependencies werden automatisch injected
  • Type Safety: Value Objects für alle Policies
  • Readonly Classes: Unveränderliche Policies für Thread-Safety
  • Framework-konform: Nutzt bestehende Patterns (IpAuthPolicy, RouteNotFound)