feat(filesystem): introduce FileOwnership and ProcessUser value objects
- Add `FileOwnership` to encapsulate file owner and group information. - Add `ProcessUser` to represent and manage system process user details. - Enhance ownership matching and debugging with structured data objects. - Include new documentation on file ownership handling and permission improvements. - Prepare infrastructure for enriched error handling in filesystem operations.
This commit is contained in:
226
docs/FILEPERMISSION_EXCEPTION_IMPROVEMENT_PLAN.md
Normal file
226
docs/FILEPERMISSION_EXCEPTION_IMPROVEMENT_PLAN.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# FilePermissionException Verbesserungs-Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Verbesserung der `FilePermissionException` um detaillierte Permission- und User-Informationen f?r besseres Debugging.
|
||||||
|
|
||||||
|
## Problem-Analyse
|
||||||
|
|
||||||
|
### Aktueller Zustand
|
||||||
|
|
||||||
|
Die `FilePermissionException` gibt momentan nur minimale Informationen:
|
||||||
|
|
||||||
|
```php
|
||||||
|
FilePermissionException::delete($path, 'Directory is not writable: ' . $dir);
|
||||||
|
// Output: "Permission denied for delete on file: /path/to/file (Directory is not writable: /path/to/dir)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fehlende Informationen:**
|
||||||
|
- Aktuelle Permissions der Datei/Verzeichnisses
|
||||||
|
- Owner/Group der Datei
|
||||||
|
- Aktueller Process-User
|
||||||
|
- Erwartete Permissions
|
||||||
|
- Octal-Darstellung der Permissions
|
||||||
|
|
||||||
|
### Verf?gbare Infrastruktur
|
||||||
|
|
||||||
|
? **PermissionChecker** hat bereits `getDiagnosticInfo()`:
|
||||||
|
- Liefert owner, group, permissions, parent_dir info
|
||||||
|
- Nutzt `posix_getpwuid()` und `posix_getgrgid()`
|
||||||
|
|
||||||
|
? **ExceptionContext** hat `withData()`, `withDebug()`, `withMetadata()`
|
||||||
|
|
||||||
|
? **posix_geteuid()** verf?gbar f?r Process-User
|
||||||
|
|
||||||
|
## L?sungsplan
|
||||||
|
|
||||||
|
### L?sung 1: Erweitere FilePermissionException (HOHE PRIORIT?T)
|
||||||
|
|
||||||
|
**Neue statische Factory-Methoden mit Permission-Details:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
public static function delete(
|
||||||
|
string $path,
|
||||||
|
?string $reason = null,
|
||||||
|
?PermissionChecker $permissionChecker = null
|
||||||
|
): self {
|
||||||
|
$diagnosticInfo = $permissionChecker?->getDiagnosticInfo($path) ?? [];
|
||||||
|
$currentUser = self::getCurrentUserInfo();
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
path: $path,
|
||||||
|
operation: 'delete',
|
||||||
|
reason: $reason,
|
||||||
|
diagnosticInfo: $diagnosticInfo,
|
||||||
|
currentUser: $currentUser
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Neue Properties:**
|
||||||
|
- `?array $diagnosticInfo` - Permission-Details vom PermissionChecker
|
||||||
|
- `?array $currentUser` - Aktueller Process-User Info
|
||||||
|
|
||||||
|
**Verbesserte Message-Formatierung:**
|
||||||
|
```php
|
||||||
|
$message = "Permission denied for {$operation} on file: {$path}";
|
||||||
|
if ($reason) {
|
||||||
|
$message .= " ({$reason})";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add detailed info if available
|
||||||
|
if ($this->diagnosticInfo) {
|
||||||
|
$message .= sprintf(
|
||||||
|
"\n File owner: %s, group: %s, permissions: %s",
|
||||||
|
$this->diagnosticInfo['owner'] ?? 'unknown',
|
||||||
|
$this->diagnosticInfo['group'] ?? 'unknown',
|
||||||
|
$this->diagnosticInfo['permissions'] ?? 'unknown'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($this->diagnosticInfo['parent_dir'])) {
|
||||||
|
$parentInfo = $permissionChecker?->getDiagnosticInfo($this->diagnosticInfo['parent_dir']) ?? [];
|
||||||
|
$message .= sprintf(
|
||||||
|
"\n Parent directory: %s (owner: %s, permissions: %s)",
|
||||||
|
$this->diagnosticInfo['parent_dir'],
|
||||||
|
$parentInfo['owner'] ?? 'unknown',
|
||||||
|
$parentInfo['permissions'] ?? 'unknown'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->currentUser) {
|
||||||
|
$message .= sprintf(
|
||||||
|
"\n Current process user: %s (uid: %d, gid: %d)",
|
||||||
|
$this->currentUser['name'],
|
||||||
|
$this->currentUser['uid'],
|
||||||
|
$this->currentUser['gid']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### L?sung 2: Helper-Methode f?r Process-User Info
|
||||||
|
|
||||||
|
**Neue private Methode in FilePermissionException:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
private static function getCurrentUserInfo(): ?array
|
||||||
|
{
|
||||||
|
if (!function_exists('posix_geteuid')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uid = posix_geteuid();
|
||||||
|
$gid = posix_getegid();
|
||||||
|
$userInfo = posix_getpwuid($uid);
|
||||||
|
$groupInfo = posix_getgrgid($gid);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uid' => $uid,
|
||||||
|
'gid' => $gid,
|
||||||
|
'name' => $userInfo['name'] ?? 'unknown',
|
||||||
|
'group' => $groupInfo['name'] ?? 'unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### L?sung 3: Integration in FileStorage-Methoden
|
||||||
|
|
||||||
|
**Anpassung aller FilePermissionException Aufrufe:**
|
||||||
|
|
||||||
|
**Vorher:**
|
||||||
|
```php
|
||||||
|
if (! is_writable($dir)) {
|
||||||
|
throw FilePermissionException::delete($path, 'Directory is not writable: ' . $dir);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nachher:**
|
||||||
|
```php
|
||||||
|
if (! is_writable($dir)) {
|
||||||
|
throw FilePermissionException::delete(
|
||||||
|
path: $path,
|
||||||
|
reason: 'Directory is not writable: ' . $dir,
|
||||||
|
permissionChecker: $this->permissions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Betroffene Stellen:**
|
||||||
|
1. `FileStorage::get()` - read exception
|
||||||
|
2. `FileStorage::put()` - write exception, createDirectory exception
|
||||||
|
3. `FileStorage::delete()` - delete exception
|
||||||
|
4. `FileStorage::createDirectory()` - createDirectory exception
|
||||||
|
5. Alle anderen Stellen, die FilePermissionException werfen
|
||||||
|
|
||||||
|
### L?sung 4: ExceptionContext erweitern
|
||||||
|
|
||||||
|
**Erweiterte Context-Daten in Exception:**
|
||||||
|
|
||||||
|
```php
|
||||||
|
$context = ExceptionContext::forOperation('file.permission', 'filesystem')
|
||||||
|
->withData([
|
||||||
|
'path' => $path,
|
||||||
|
'operation' => $operation,
|
||||||
|
'reason' => $reason,
|
||||||
|
])
|
||||||
|
->withDebug([
|
||||||
|
'file_permissions' => $diagnosticInfo['permissions'] ?? null,
|
||||||
|
'file_owner' => $diagnosticInfo['owner'] ?? null,
|
||||||
|
'file_group' => $diagnosticInfo['group'] ?? null,
|
||||||
|
'parent_dir' => $diagnosticInfo['parent_dir'] ?? null,
|
||||||
|
'parent_writable' => $diagnosticInfo['parent_writable'] ?? null,
|
||||||
|
])
|
||||||
|
->withMetadata([
|
||||||
|
'current_user' => $currentUser,
|
||||||
|
'file_exists' => $diagnosticInfo['exists'] ?? false,
|
||||||
|
'is_file' => $diagnosticInfo['is_file'] ?? false,
|
||||||
|
'is_dir' => $diagnosticInfo['is_dir'] ?? false,
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementierungsreihenfolge
|
||||||
|
|
||||||
|
1. ? **L?sung 2** - Helper-Methode f?r Process-User (einfach, keine Dependencies)
|
||||||
|
2. ? **L?sung 1** - Erweitere FilePermissionException Constructor und Factory-Methoden
|
||||||
|
3. ? **L?sung 3** - Integration in FileStorage-Methoden
|
||||||
|
4. ? **L?sung 4** - ExceptionContext erweitern
|
||||||
|
|
||||||
|
## Beispiel-Output
|
||||||
|
|
||||||
|
### Vorher:
|
||||||
|
```
|
||||||
|
Permission denied for delete on file: /var/www/html/storage/cache/file.cache.php (Directory is not writable: /var/www/html/storage/cache)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nachher:
|
||||||
|
```
|
||||||
|
Permission denied for delete on file: /var/www/html/storage/cache/file.cache.php (Directory is not writable: /var/www/html/storage/cache)
|
||||||
|
File owner: www-data, group: www-data, permissions: drwxrwxr-x
|
||||||
|
Parent directory: /var/www/html/storage/cache (owner: root, permissions: drwxr-xr-x)
|
||||||
|
Current process user: www-data (uid: 33, gid: 33)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vorteile
|
||||||
|
|
||||||
|
1. **Besseres Debugging**: Sofort sichtbar, welche Permissions falsch sind
|
||||||
|
2. **User-Info**: Zeigt Owner-Mismatch sofort
|
||||||
|
3. **Context**: Vollst?ndige Permission-Info in Exception-Context f?r Logging
|
||||||
|
4. **R?ckw?rtskompatibel**: Alte Aufrufe funktionieren weiterhin (optional Parameter)
|
||||||
|
|
||||||
|
## Performance-?berlegungen
|
||||||
|
|
||||||
|
- `getDiagnosticInfo()` ist relativ teuer (mehrere `stat()` Calls)
|
||||||
|
- **L?sung**: Nur bei Exception-Throw aufrufen (nicht bei jedem Check)
|
||||||
|
- Optional: K?nnte in `withDebug()` ausgelagert werden, wenn Performance kritisch ist
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
1. **Unit-Tests**: FilePermissionException mit Permission-Details
|
||||||
|
2. **Integration-Tests**: FileStorage-Methoden mit verschiedenen Permission-Szenarien
|
||||||
|
3. **E2E-Tests**: Cache-Operationen mit Permission-Problemen
|
||||||
|
|
||||||
|
## Dokumentation
|
||||||
|
|
||||||
|
Nach Implementierung aktualisieren:
|
||||||
|
- `docs/claude/error-handling.md` - FilePermissionException mit Details
|
||||||
|
- `docs/deployment/cache-configuration.md` - Exception-Beispiele
|
||||||
388
docs/REDIS_ACL_EXPLANATION.md
Normal file
388
docs/REDIS_ACL_EXPLANATION.md
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
# Redis ACL (Access Control List) - Erkl?rung
|
||||||
|
|
||||||
|
## Was ist Redis ACL?
|
||||||
|
|
||||||
|
**Redis ACL (Access Control List)** ist ein Authentifizierungs- und Autorisierungssystem, das in Redis 6.0 eingef?hrt wurde. Es erm?glicht feingranulare Kontrolle ?ber Benutzerrechte und Zugriff auf Redis-Befehle und -Daten.
|
||||||
|
|
||||||
|
### Unterschied: Legacy vs. ACL
|
||||||
|
|
||||||
|
#### Legacy Authentication (vor Redis 6.0)
|
||||||
|
|
||||||
|
**Wie es funktioniert:**
|
||||||
|
- Ein einzelnes Passwort f?r die gesamte Redis-Instanz
|
||||||
|
- Konfiguration via `requirepass` in `redis.conf` oder `--requirepass` Parameter
|
||||||
|
- Alle verbundenen Clients haben **vollst?ndigen Zugriff** auf alle Daten und Befehle
|
||||||
|
- Keine Unterscheidung zwischen Benutzern
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
# redis.conf
|
||||||
|
requirepass "mein-passwort-123"
|
||||||
|
|
||||||
|
# Oder via Command Line
|
||||||
|
redis-server --requirepass "mein-passwort-123"
|
||||||
|
```
|
||||||
|
|
||||||
|
**PHP Code:**
|
||||||
|
```php
|
||||||
|
$redis = new Redis();
|
||||||
|
$redis->connect('localhost', 6379);
|
||||||
|
$redis->auth('mein-passwort-123'); // Einfache Passwort-Auth
|
||||||
|
// Jetzt hat der Client VOLLST?NDIGEN Zugriff
|
||||||
|
```
|
||||||
|
|
||||||
|
**Probleme:**
|
||||||
|
- ? Keine Benutzer-Trennung
|
||||||
|
- ? Keine feingranulare Kontrolle
|
||||||
|
- ? Alle Clients haben Admin-Rechte
|
||||||
|
- ? Keine M?glichkeit, verschiedene Berechtigungen zu vergeben
|
||||||
|
|
||||||
|
#### Redis ACL (ab Redis 6.0)
|
||||||
|
|
||||||
|
**Wie es funktioniert:**
|
||||||
|
- Mehrere Benutzer mit unterschiedlichen Berechtigungen
|
||||||
|
- Jeder Benutzer hat Username + Passwort
|
||||||
|
- Feingranulare Kontrolle ?ber:
|
||||||
|
- Welche Befehle erlaubt sind
|
||||||
|
- Auf welche Keys/Datenbanken zugegriffen werden kann
|
||||||
|
- Lese-/Schreib-Rechte
|
||||||
|
- Spezifische Key-Patterns
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
# Redis CLI
|
||||||
|
ACL SETUSER cache-user on >cache-password123 ~cache:* +get +set +del +exists
|
||||||
|
ACL SETUSER queue-user on >queue-password456 ~queue:* +lpush +rpop +llen
|
||||||
|
ACL SETUSER admin on >admin-password789 ~* &* +@all
|
||||||
|
```
|
||||||
|
|
||||||
|
**PHP Code:**
|
||||||
|
```php
|
||||||
|
// F?r Cache-Benutzer
|
||||||
|
$redis = new Redis();
|
||||||
|
$redis->connect('localhost', 6379);
|
||||||
|
$redis->auth('cache-user', 'cache-password123'); // Username + Password
|
||||||
|
|
||||||
|
// F?r Admin
|
||||||
|
$redis->auth('admin', 'admin-password789'); // Username + Password
|
||||||
|
```
|
||||||
|
|
||||||
|
## ACL Konzepte im Detail
|
||||||
|
|
||||||
|
### 1. Benutzer (Users)
|
||||||
|
|
||||||
|
**Standard-Benutzer:**
|
||||||
|
- `default`: Voreingestellter Benutzer (ohne Passwort = kein Auth erforderlich)
|
||||||
|
- Benutzer k?nnen aktiv (`on`) oder deaktiviert (`off`) sein
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```redis
|
||||||
|
ACL SETUSER myuser on >mypassword
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Passw?rter
|
||||||
|
|
||||||
|
**Passwort-Formate:**
|
||||||
|
- Plaintext: `>password123`
|
||||||
|
- Hash: `#5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8`
|
||||||
|
|
||||||
|
**Mehrere Passw?rter:**
|
||||||
|
```redis
|
||||||
|
ACL SETUSER myuser on >password1 >password2 >password3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Key-Patterns (Zugriff auf Keys)
|
||||||
|
|
||||||
|
**Pattern-Syntax:**
|
||||||
|
- `~*`: Zugriff auf alle Keys
|
||||||
|
- `~cache:*`: Zugriff nur auf Keys die mit `cache:` beginnen
|
||||||
|
- `~user:*`: Zugriff nur auf Keys die mit `user:` beginnen
|
||||||
|
- `~{user,cache}:*`: Zugriff auf mehrere Patterns
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```redis
|
||||||
|
ACL SETUSER cache-user on >pass ~cache:* ~session:*
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Befehle (Commands)
|
||||||
|
|
||||||
|
**Berechtigungen:**
|
||||||
|
- `+command`: Befehl erlauben (z.B. `+get`, `+set`)
|
||||||
|
- `-command`: Befehl verbieten (z.B. `-flushdb`)
|
||||||
|
- `+@category`: Alle Befehle einer Kategorie erlauben
|
||||||
|
- `-@category`: Alle Befehle einer Kategorie verbieten
|
||||||
|
- `+@all`: Alle Befehle erlauben (Admin)
|
||||||
|
- `+@read`: Nur Lese-Befehle
|
||||||
|
- `+@write`: Nur Schreib-Befehle
|
||||||
|
- `+@keyspace`: Keyspace-Befehle
|
||||||
|
- `+@string`: String-Befehle
|
||||||
|
- `+@list`: List-Befehle
|
||||||
|
- `+@set`: Set-Befehle
|
||||||
|
- `+@sortedset`: Sorted Set-Befehle
|
||||||
|
- `+@hash`: Hash-Befehle
|
||||||
|
- `+@stream`: Stream-Befehle
|
||||||
|
- `+@pubsub`: Pub/Sub-Befehle
|
||||||
|
- `+@transaction`: Transaction-Befehle
|
||||||
|
- `+@connection`: Connection-Befehle
|
||||||
|
- `+@scripting`: Scripting-Befehle
|
||||||
|
- `+@admin`: Admin-Befehle
|
||||||
|
- `+@dangerous`: Gef?hrliche Befehle (flush, debug, etc.)
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```redis
|
||||||
|
# Cache-User: Nur GET, SET, DEL, EXISTS
|
||||||
|
ACL SETUSER cache-user on >pass ~cache:* +get +set +del +exists
|
||||||
|
|
||||||
|
# Queue-User: Nur List-Operationen
|
||||||
|
ACL SETUSER queue-user on >pass ~queue:* +lpush +rpop +llen +lrange
|
||||||
|
|
||||||
|
# Admin: Alle Befehle
|
||||||
|
ACL SETUSER admin on >pass ~* &* +@all
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Datenbanken (Databases)
|
||||||
|
|
||||||
|
**Standard:**
|
||||||
|
- Alle Datenbanken sind erlaubt (`&*`)
|
||||||
|
- Spezifische Datenbanken: `&0`, `&1`, `&2`, etc.
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```redis
|
||||||
|
# Nur Datenbank 1 erlauben
|
||||||
|
ACL SETUSER myuser on >pass ~* &1 +@all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Praktische Beispiele
|
||||||
|
|
||||||
|
### Beispiel 1: Cache-User (nur Lese/Schreib auf Cache-Keys)
|
||||||
|
|
||||||
|
```redis
|
||||||
|
ACL SETUSER cache-user on >cache-secret-password ~cache:* +get +set +del +exists +expire +ttl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Was darf dieser User?**
|
||||||
|
- ? `GET cache:user:123`
|
||||||
|
- ? `SET cache:user:123 "value"`
|
||||||
|
- ? `DEL cache:user:123`
|
||||||
|
- ? `EXISTS cache:user:123`
|
||||||
|
- ? `GET user:123` (nicht `cache:` Prefix)
|
||||||
|
- ? `FLUSHDB` (nicht erlaubt)
|
||||||
|
- ? `KEYS *` (nicht erlaubt)
|
||||||
|
|
||||||
|
### Beispiel 2: Queue-User (nur Queue-Operationen)
|
||||||
|
|
||||||
|
```redis
|
||||||
|
ACL SETUSER queue-user on >queue-secret-password ~queue:* +lpush +rpop +llen +lrange +lrem +@list
|
||||||
|
```
|
||||||
|
|
||||||
|
**Was darf dieser User?**
|
||||||
|
- ? `LPUSH queue:emails "task"`
|
||||||
|
- ? `RPOP queue:emails`
|
||||||
|
- ? `LLEN queue:emails`
|
||||||
|
- ? `GET queue:emails` (String-Befehle nicht erlaubt)
|
||||||
|
- ? `FLUSHDB` (nicht erlaubt)
|
||||||
|
|
||||||
|
### Beispiel 3: Admin-User (vollst?ndiger Zugriff)
|
||||||
|
|
||||||
|
```redis
|
||||||
|
ACL SETUSER admin on >admin-secret-password ~* &* +@all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Was darf dieser User?**
|
||||||
|
- ? Alle Befehle
|
||||||
|
- ? Alle Keys
|
||||||
|
- ? Alle Datenbanken
|
||||||
|
|
||||||
|
### Beispiel 4: Read-Only User (nur Lese-Zugriff)
|
||||||
|
|
||||||
|
```redis
|
||||||
|
ACL SETUSER readonly-user on >readonly-password ~* +@read
|
||||||
|
```
|
||||||
|
|
||||||
|
**Was darf dieser User?**
|
||||||
|
- ? `GET`, `HGET`, `SMEMBERS`, etc. (Lese-Befehle)
|
||||||
|
- ? `SET`, `DEL`, `FLUSHDB` (Schreib-Befehle)
|
||||||
|
|
||||||
|
## ACL vs. Legacy in unserem Framework
|
||||||
|
|
||||||
|
### Aktuelle Implementierung (Legacy)
|
||||||
|
|
||||||
|
**Konfiguration:**
|
||||||
|
```yaml
|
||||||
|
# docker-compose.staging.yml
|
||||||
|
redis-server --requirepass "password123"
|
||||||
|
```
|
||||||
|
|
||||||
|
**PHP Code:**
|
||||||
|
```php
|
||||||
|
// RedisConfig.php
|
||||||
|
password: $env->get(EnvKey::REDIS_PASSWORD, null)
|
||||||
|
|
||||||
|
// RedisConnection.php
|
||||||
|
$redis->auth($this->config->password); // Einfache Passwort-Auth
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:**
|
||||||
|
- Alle Verbindungen (cache, queue, session) verwenden dasselbe Passwort
|
||||||
|
- Alle haben vollst?ndigen Zugriff auf alle Daten
|
||||||
|
- Keine Isolation zwischen verschiedenen Use Cases
|
||||||
|
|
||||||
|
### Potenzielle ACL-Implementierung
|
||||||
|
|
||||||
|
**Konfiguration:**
|
||||||
|
```yaml
|
||||||
|
# docker-compose.staging.yml
|
||||||
|
# Redis startet mit ACL
|
||||||
|
# ACL wird via init script oder config gesetzt:
|
||||||
|
ACL SETUSER cache-user on >cache-password ~cache:* +@read +@write -@dangerous
|
||||||
|
ACL SETUSER queue-user on >queue-password ~queue:* +@list +@read -@dangerous
|
||||||
|
ACL SETUSER session-user on >session-password ~session:* +@read +@write -@dangerous
|
||||||
|
ACL SETUSER admin on >admin-password ~* &* +@all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment Variables:**
|
||||||
|
```env
|
||||||
|
# F?r Cache-Connection
|
||||||
|
REDIS_USERNAME=cache-user
|
||||||
|
REDIS_PASSWORD=cache-password
|
||||||
|
|
||||||
|
# F?r Queue-Connection
|
||||||
|
REDIS_USERNAME=queue-user
|
||||||
|
REDIS_PASSWORD=queue-password
|
||||||
|
|
||||||
|
# F?r Session-Connection
|
||||||
|
REDIS_USERNAME=session-user
|
||||||
|
REDIS_PASSWORD=session-password
|
||||||
|
```
|
||||||
|
|
||||||
|
**PHP Code:**
|
||||||
|
```php
|
||||||
|
// RedisConfig.php
|
||||||
|
public ?string $username = null;
|
||||||
|
public ?string $password = null;
|
||||||
|
|
||||||
|
// RedisConnection.php
|
||||||
|
if ($this->config->password) {
|
||||||
|
if ($this->config->username) {
|
||||||
|
// ACL: auth(username, password)
|
||||||
|
$authResult = $this->client->auth(
|
||||||
|
$this->config->username,
|
||||||
|
$this->config->password
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Legacy: auth(password)
|
||||||
|
$authResult = $this->client->auth($this->config->password);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- ? Isolation: Cache-User kann nicht auf Queue-Keys zugreifen
|
||||||
|
- ? Sicherheit: Minimale Berechtigungen (Principle of Least Privilege)
|
||||||
|
- ? Audit: Verschiedene User f?r verschiedene Operationen
|
||||||
|
- ? Schutz: Kein User kann `FLUSHDB` ausf?hren (au?er Admin)
|
||||||
|
|
||||||
|
## Wann ACL verwenden?
|
||||||
|
|
||||||
|
### ACL ist sinnvoll wenn:
|
||||||
|
|
||||||
|
1. **Multi-Tenant Umgebungen**
|
||||||
|
- Verschiedene Anwendungen teilen sich Redis
|
||||||
|
- Jede Anwendung braucht isolierten Zugriff
|
||||||
|
|
||||||
|
2. **Sicherheits-Anforderungen**
|
||||||
|
- Compliance-Anforderungen (z.B. PCI-DSS)
|
||||||
|
- Feingranulare Zugriffskontrolle erforderlich
|
||||||
|
|
||||||
|
3. **Team-Isolation**
|
||||||
|
- Verschiedene Teams haben verschiedene Bereiche
|
||||||
|
- Cache-Team, Queue-Team, Session-Team
|
||||||
|
|
||||||
|
4. **Production vs. Staging**
|
||||||
|
- Staging: Weniger restriktiv
|
||||||
|
- Production: Strenge ACL-Regeln
|
||||||
|
|
||||||
|
### Legacy Auth ist ausreichend wenn:
|
||||||
|
|
||||||
|
1. **Single-Application**
|
||||||
|
- Nur eine Anwendung nutzt Redis
|
||||||
|
- Keine Multi-Tenant-Anforderungen
|
||||||
|
|
||||||
|
2. **Einfache Setups**
|
||||||
|
- Entwicklungs-Umgebungen
|
||||||
|
- Kleine Projekte ohne hohe Sicherheitsanforderungen
|
||||||
|
|
||||||
|
3. **Externe Isolation**
|
||||||
|
- Redis l?uft in isoliertem Netzwerk
|
||||||
|
- Zugriff wird auf Netzwerk-Ebene kontrolliert
|
||||||
|
|
||||||
|
## Migration von Legacy zu ACL
|
||||||
|
|
||||||
|
### Schritt 1: Redis mit ACL konfigurieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Redis startet mit Legacy Auth
|
||||||
|
# Dann ACL konfigurieren:
|
||||||
|
|
||||||
|
# Legacy User erstellen (f?r Backward Compatibility)
|
||||||
|
ACL SETUSER default on >legacy-password ~* &* +@all
|
||||||
|
|
||||||
|
# Spezialisierte User erstellen
|
||||||
|
ACL SETUSER cache-user on >cache-password ~cache:* +@read +@write
|
||||||
|
ACL SETUSER queue-user on >queue-password ~queue:* +@list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 2: Code anpassen
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Schrittweise Migration:
|
||||||
|
// 1. Code unterst?tzt beide Methoden (Backward Compatible)
|
||||||
|
// 2. Neue Connections verwenden ACL
|
||||||
|
// 3. Alte Connections k?nnen weiterhin Legacy Auth verwenden
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 3: Testing
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Test mit verschiedenen Usern
|
||||||
|
// Verifizieren dass Isolation funktioniert
|
||||||
|
// Verifizieren dass Legacy Auth noch funktioniert
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
| Aspekt | Legacy Auth | Redis ACL |
|
||||||
|
|--------|-------------|-----------|
|
||||||
|
| **Benutzer** | 1 (implizit) | Viele (explizit) |
|
||||||
|
| **Passwort** | 1 Passwort f?r alle | Passwort pro User |
|
||||||
|
| **Berechtigungen** | Alles oder nichts | Feingranular |
|
||||||
|
| **Key-Isolation** | Nein | Ja (Patterns) |
|
||||||
|
| **Befehl-Kontrolle** | Nein | Ja (pro Command) |
|
||||||
|
| **Komplexit?t** | Niedrig | Mittel-Hoch |
|
||||||
|
| **Sicherheit** | Basis | Hoch |
|
||||||
|
| **Verwaltung** | Einfach | Komplexer |
|
||||||
|
|
||||||
|
## Empfehlung f?r unser Framework
|
||||||
|
|
||||||
|
**Aktuell (Staging):**
|
||||||
|
- Legacy Auth ist ausreichend
|
||||||
|
- Einfacher zu verwalten
|
||||||
|
- Keine Multi-Tenant-Anforderungen
|
||||||
|
|
||||||
|
**Zukunft (Production):**
|
||||||
|
- ACL k?nnte sinnvoll sein f?r:
|
||||||
|
- Bessere Isolation zwischen Cache/Queue/Session
|
||||||
|
- Compliance-Anforderungen
|
||||||
|
- Multi-Tenant-Szenarien
|
||||||
|
|
||||||
|
**Code-Vorbereitung:**
|
||||||
|
- Code sollte beide Methoden unterst?tzen (Backward Compatible)
|
||||||
|
- `REDIS_USERNAME` optional machen
|
||||||
|
- Wenn `username` gesetzt ? ACL Auth
|
||||||
|
- Wenn nur `password` ? Legacy Auth
|
||||||
|
|
||||||
|
## Weitere Ressourcen
|
||||||
|
|
||||||
|
- [Redis ACL Documentation](https://redis.io/docs/management/security/acl/)
|
||||||
|
- [Redis ACL Commands](https://redis.io/commands/acl/)
|
||||||
|
- [Redis Security Best Practices](https://redis.io/docs/management/security/)
|
||||||
364
docs/REDIS_AUTHENTICATION_ANALYSIS.md
Normal file
364
docs/REDIS_AUTHENTICATION_ANALYSIS.md
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# Redis Authentication Problem Analysis & Refactoring Plan
|
||||||
|
|
||||||
|
## Problem Description
|
||||||
|
|
||||||
|
**Fehlermeldung:**
|
||||||
|
```
|
||||||
|
Direct Redis connection failed: Failed to connect to Redis (cache): WRONGPASS invalid username-password pair or user is disabled.
|
||||||
|
with Host: staging-redis and Password: gRUFGtFFthCGFHYjZ7o9yuiDRJ0JdG4Syiv1fVPb544=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
### Identified Issues
|
||||||
|
|
||||||
|
1. **Password Whitespace/Newline Problem**
|
||||||
|
- Das Passwort wird aus Docker Secrets geladen (`/run/secrets/redis_password`)
|
||||||
|
- `DockerSecretsResolver` verwendet `trim()`, aber m?glicherweise gibt es unsichtbare Zeichen (z.B. `\r\n`, `\n`, Tabs)
|
||||||
|
- Redis ist sehr strikt bei Passwort-?bereinstimmungen
|
||||||
|
|
||||||
|
2. **Fehlende Passwort-Validierung**
|
||||||
|
- Keine Validierung des Passworts vor der Verwendung
|
||||||
|
- Keine Sanitization au?erhalb von `trim()`
|
||||||
|
|
||||||
|
3. **Unzureichende Fehlerbehandlung**
|
||||||
|
- Fehlermeldung enth?lt das vollst?ndige Passwort (Security Risk!)
|
||||||
|
- Keine Unterscheidung zwischen verschiedenen Auth-Fehlertypen
|
||||||
|
- Keine detaillierte Diagnose-Informationen
|
||||||
|
|
||||||
|
4. **Keine Redis ACL Unterst?tzung**
|
||||||
|
- Aktuell nur Legacy Passwort-Auth (`requirepass`)
|
||||||
|
- Redis 6+ unterst?tzt ACL mit Username/Password
|
||||||
|
- Code ist nicht f?r zuk?nftige ACL-Nutzung vorbereitet
|
||||||
|
|
||||||
|
5. **Fehlende Passwort-Normalisierung**
|
||||||
|
- Verschiedene Whitespace-Zeichen werden nicht normalisiert
|
||||||
|
- Keine Behandlung von UTF-8 BOM oder anderen Steuerzeichen
|
||||||
|
|
||||||
|
## Current Implementation Analysis
|
||||||
|
|
||||||
|
### Code Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Environment::get(REDIS_PASSWORD)
|
||||||
|
?
|
||||||
|
DockerSecretsResolver::resolve()
|
||||||
|
?
|
||||||
|
file_get_contents() + trim()
|
||||||
|
?
|
||||||
|
RedisConfig::fromEnvironment()
|
||||||
|
?
|
||||||
|
RedisConnection::connect()
|
||||||
|
?
|
||||||
|
$client->auth($password) ? WRONGPASS Fehler hier
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problematic Code Sections
|
||||||
|
|
||||||
|
**1. DockerSecretsResolver.php (Zeile 104)**
|
||||||
|
```php
|
||||||
|
$result = trim($content);
|
||||||
|
```
|
||||||
|
- Problem: Entfernt nur Whitespace am Anfang/Ende
|
||||||
|
- Fehlt: Normalisierung von `\r\n` zu `\n`, Entfernung von unsichtbaren Zeichen
|
||||||
|
|
||||||
|
**2. RedisConnection.php (Zeile 92-95)**
|
||||||
|
```php
|
||||||
|
if ($this->config->password) {
|
||||||
|
if (! $this->client->auth($this->config->password)) {
|
||||||
|
throw new RedisConnectionException("Redis authentication failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Problem: Generische Fehlermeldung ohne Details
|
||||||
|
- Fehlt: Unterscheidung zwischen verschiedenen Auth-Fehlern
|
||||||
|
- Fehlt: Passwort-Validierung vor Auth-Versuch
|
||||||
|
|
||||||
|
**3. RedisConnection.php (Zeile 115)**
|
||||||
|
```php
|
||||||
|
throw new RedisConnectionException(
|
||||||
|
"Failed to connect to Redis ({$this->name}): " . $e->getMessage() . " with Host: {$this->config->host} and Password: {$this->config->password}",
|
||||||
|
previous: $e
|
||||||
|
);
|
||||||
|
```
|
||||||
|
- **KRITISCH**: Passwort wird in Exception-Message ausgegeben (Security Risk!)
|
||||||
|
- Passwort sollte niemals in Logs/Exceptions erscheinen
|
||||||
|
|
||||||
|
## Refactoring Vorschl?ge
|
||||||
|
|
||||||
|
### 1. Passwort-Normalisierung verbessern
|
||||||
|
|
||||||
|
**Problem:** `trim()` ist unzureichend f?r Docker Secrets
|
||||||
|
|
||||||
|
**L?sung:** Erweiterte Normalisierung in `DockerSecretsResolver`
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function normalizeSecret(string $content): string
|
||||||
|
{
|
||||||
|
// Entferne alle Whitespace-Zeichen am Anfang und Ende
|
||||||
|
$normalized = trim($content);
|
||||||
|
|
||||||
|
// Entferne unsichtbare Steuerzeichen (au?er normale Zeichen)
|
||||||
|
$normalized = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $normalized);
|
||||||
|
|
||||||
|
// Normalisiere Line Endings (falls vorhanden)
|
||||||
|
$normalized = str_replace(["\r\n", "\r"], "", $normalized);
|
||||||
|
|
||||||
|
// Entferne UTF-8 BOM falls vorhanden
|
||||||
|
if (str_starts_with($normalized, "\xEF\xBB\xBF")) {
|
||||||
|
$normalized = substr($normalized, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Passwort-Validierung hinzuf?gen
|
||||||
|
|
||||||
|
**Problem:** Keine Validierung vor Verwendung
|
||||||
|
|
||||||
|
**L?sung:** Validierung in `RedisConfig`
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function validatePassword(?string $password): ?string
|
||||||
|
{
|
||||||
|
if ($password === null || $password === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnung f?r sehr kurze Passw?rter
|
||||||
|
if (strlen($password) < 8) {
|
||||||
|
// Log warning, aber nicht blockieren (kann gewollt sein)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warnung f?r Whitespace in der Mitte (nur trimmen, nicht blockieren)
|
||||||
|
$trimmed = trim($password);
|
||||||
|
if ($trimmed !== $password) {
|
||||||
|
// Log warning: Password hatte Whitespace am Anfang/Ende
|
||||||
|
}
|
||||||
|
|
||||||
|
return $trimmed !== '' ? $trimmed : null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Sicherheit: Passwort aus Exceptions entfernen
|
||||||
|
|
||||||
|
**Problem:** Passwort erscheint in Exception-Messages
|
||||||
|
|
||||||
|
**L?sung:** Maskiertes Passwort in Fehlermeldungen
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function maskPassword(?string $password): string
|
||||||
|
{
|
||||||
|
if ($password === null || $password === '') {
|
||||||
|
return '(not set)';
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = strlen($password);
|
||||||
|
if ($length <= 4) {
|
||||||
|
return str_repeat('*', $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($password, 0, 2) . str_repeat('*', $length - 4) . substr($password, -2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Verbesserte Fehlerbehandlung
|
||||||
|
|
||||||
|
**Problem:** Generische Fehlermeldungen ohne Kontext
|
||||||
|
|
||||||
|
**L?sung:** Detaillierte Exception mit diagnostischen Informationen
|
||||||
|
|
||||||
|
```php
|
||||||
|
private function authenticate(): void
|
||||||
|
{
|
||||||
|
if (!$this->config->password) {
|
||||||
|
return; // Keine Authentifizierung erforderlich
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$authResult = $this->client->auth($this->config->password);
|
||||||
|
if (!$authResult) {
|
||||||
|
throw new RedisConnectionException(
|
||||||
|
"Redis authentication failed for connection '{$this->name}'",
|
||||||
|
context: $this->createAuthDiagnostics()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (RedisException $e) {
|
||||||
|
$message = $e->getMessage();
|
||||||
|
|
||||||
|
// Spezifische Fehlermeldungen basierend auf Redis-Fehlercode
|
||||||
|
if (str_contains($message, 'WRONGPASS')) {
|
||||||
|
throw new RedisConnectionException(
|
||||||
|
"Redis authentication failed: Invalid password for connection '{$this->name}'",
|
||||||
|
context: $this->createAuthDiagnostics(),
|
||||||
|
previous: $e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Andere Auth-Fehler
|
||||||
|
throw new RedisConnectionException(
|
||||||
|
"Redis authentication error for connection '{$this->name}': {$message}",
|
||||||
|
context: $this->createAuthDiagnostics(),
|
||||||
|
previous: $e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createAuthDiagnostics(): ExceptionContext
|
||||||
|
{
|
||||||
|
$context = ExceptionContext::forOperation('Redis Authentication');
|
||||||
|
|
||||||
|
$context->add('connection_name', $this->name);
|
||||||
|
$context->add('host', $this->config->host);
|
||||||
|
$context->add('port', $this->config->port);
|
||||||
|
$context->add('database', $this->config->database);
|
||||||
|
$context->add('password_set', $this->config->password !== null);
|
||||||
|
$context->add('password_length', $this->config->password ? strlen($this->config->password) : 0);
|
||||||
|
// KEIN Passwort in Context!
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Redis ACL Unterst?tzung (Optional, f?r Zukunft)
|
||||||
|
|
||||||
|
**Problem:** Keine Unterst?tzung f?r Redis 6+ ACL
|
||||||
|
|
||||||
|
**L?sung:** Optionale Username/Password Auth
|
||||||
|
|
||||||
|
```php
|
||||||
|
final readonly class RedisConfig
|
||||||
|
{
|
||||||
|
private function __construct(
|
||||||
|
public string $host = 'redis',
|
||||||
|
public int $port = 6379,
|
||||||
|
public ?string $password = null,
|
||||||
|
public ?string $username = null, // NEU f?r ACL
|
||||||
|
// ... rest
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function fromEnvironment(Environment $env): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
// ...
|
||||||
|
password: $env->get(EnvKey::REDIS_PASSWORD, null),
|
||||||
|
username: $env->get(EnvKey::REDIS_USERNAME, null), // NEU
|
||||||
|
// ...
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In RedisConnection.php
|
||||||
|
private function authenticate(): void
|
||||||
|
{
|
||||||
|
if (!$this->config->password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis 6+ ACL: auth(username, password)
|
||||||
|
if ($this->config->username) {
|
||||||
|
$authResult = $this->client->auth($this->config->username, $this->config->password);
|
||||||
|
} else {
|
||||||
|
// Legacy: auth(password)
|
||||||
|
$authResult = $this->client->auth($this->config->password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$authResult) {
|
||||||
|
throw new RedisConnectionException(/* ... */);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. EnvKey Enum erweitern
|
||||||
|
|
||||||
|
**L?sung:** Optional `REDIS_USERNAME` f?r zuk?nftige ACL-Nutzung
|
||||||
|
|
||||||
|
```php
|
||||||
|
enum EnvKey: string
|
||||||
|
{
|
||||||
|
// ... existing ...
|
||||||
|
case REDIS_HOST = 'REDIS_HOST';
|
||||||
|
case REDIS_PORT = 'REDIS_PORT';
|
||||||
|
case REDIS_PASSWORD = 'REDIS_PASSWORD';
|
||||||
|
case REDIS_USERNAME = 'REDIS_USERNAME'; // NEU (optional)
|
||||||
|
case REDIS_PREFIX = 'REDIS_PREFIX';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementierungsplan
|
||||||
|
|
||||||
|
### Phase 1: Kritische Sicherheitsfixes (SOFORT)
|
||||||
|
|
||||||
|
1. ? **Passwort aus Exception-Messages entfernen**
|
||||||
|
- `RedisConnection.php` Zeile 115: Maskiertes Passwort verwenden
|
||||||
|
- Security Hotfix
|
||||||
|
|
||||||
|
2. ? **Verbesserte Passwort-Normalisierung**
|
||||||
|
- `DockerSecretsResolver.php`: Erweiterte `normalizeSecret()` Methode
|
||||||
|
- Behebt wahrscheinlich das Hauptproblem
|
||||||
|
|
||||||
|
### Phase 2: Robustheit (HOCH)
|
||||||
|
|
||||||
|
3. ? **Passwort-Validierung**
|
||||||
|
- `RedisConfig`: Validierung und Sanitization
|
||||||
|
- Warnungen f?r problematische Passw?rter
|
||||||
|
|
||||||
|
4. ? **Verbesserte Fehlerbehandlung**
|
||||||
|
- Spezifische Exception-Messages
|
||||||
|
- Diagnostische Informationen (ohne Passwort!)
|
||||||
|
|
||||||
|
### Phase 3: Zukunftssicherheit (MITTEL)
|
||||||
|
|
||||||
|
5. ?? **Redis ACL Unterst?tzung** (Optional)
|
||||||
|
- Nur wenn Redis ACL verwendet werden soll
|
||||||
|
- Backward-kompatibel mit Legacy Auth
|
||||||
|
|
||||||
|
## Priorisierung
|
||||||
|
|
||||||
|
| Priorit?t | Task | Impact | Effort |
|
||||||
|
|-----------|------|--------|--------|
|
||||||
|
| ?? KRITISCH | Passwort aus Exceptions entfernen | Security | Low |
|
||||||
|
| ?? KRITISCH | Passwort-Normalisierung verbessern | Fixes Bug | Low |
|
||||||
|
| ?? HOCH | Passwort-Validierung | Robustheit | Low |
|
||||||
|
| ?? HOCH | Verbesserte Fehlerbehandlung | Debugging | Medium |
|
||||||
|
| ?? MITTEL | Redis ACL Unterst?tzung | Zukunft | Medium |
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
1. **Unit Tests f?r Passwort-Normalisierung**
|
||||||
|
```php
|
||||||
|
test('normalizes password with newlines', function () {
|
||||||
|
$resolver = new DockerSecretsResolver();
|
||||||
|
// Test with \r\n, \n, \r, etc.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Integration Tests f?r Redis Auth**
|
||||||
|
```php
|
||||||
|
test('connects with password from Docker Secret', function () {
|
||||||
|
// Test with actual Redis instance
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Security Tests**
|
||||||
|
```php
|
||||||
|
test('does not expose password in exception messages', function () {
|
||||||
|
// Verify password is masked
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Outcomes
|
||||||
|
|
||||||
|
Nach Implementierung:
|
||||||
|
- ? Passwort wird korrekt normalisiert (Whitespace entfernt)
|
||||||
|
- ? Keine Passw?rter in Logs/Exceptions
|
||||||
|
- ? Bessere Fehlermeldungen f?r Debugging
|
||||||
|
- ? Robustere Passwort-Behandlung
|
||||||
|
- ? Vorbereitung f?r Redis ACL (optional)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Das Hauptproblem ist wahrscheinlich **Whitespace/Newlines im Passwort**
|
||||||
|
- Die Security-Probleme (Passwort in Exceptions) sollten sofort behoben werden
|
||||||
|
- ACL-Unterst?tzung ist optional, aber gut f?r Zukunftssicherheit
|
||||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Discovery\Exceptions;
|
namespace App\Framework\Discovery\Exceptions;
|
||||||
|
|
||||||
use App\Framework\Exception\Core\DiscoveryErrorCode;
|
|
||||||
use App\Framework\Exception\Core\FileSystemErrorCode;
|
|
||||||
use App\Framework\Exception\ErrorCode;
|
use App\Framework\Exception\ErrorCode;
|
||||||
use App\Framework\Exception\ExceptionContext;
|
use App\Framework\Exception\ExceptionContext;
|
||||||
use App\Framework\Exception\FrameworkException;
|
use App\Framework\Exception\FrameworkException;
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ final readonly class FileStreamProcessor
|
|||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Only log errors, not every processed file
|
// Only log errors, not every processed file
|
||||||
$this->logger?->warning(
|
$this->logger?->warning(
|
||||||
"Failed to process file {$file->getPath()->toString()}: {$e->getMessage()}"
|
"Failed to process file {$file->getPath()->toString()}: {$e->getMessage()} in FileStreamProcessor"
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// Always cleanup after processing a file
|
// Always cleanup after processing a file
|
||||||
|
|||||||
@@ -6,25 +6,43 @@ namespace App\Framework\Filesystem\Exceptions;
|
|||||||
|
|
||||||
use App\Framework\Exception\Core\FilesystemErrorCode;
|
use App\Framework\Exception\Core\FilesystemErrorCode;
|
||||||
use App\Framework\Exception\ExceptionContext;
|
use App\Framework\Exception\ExceptionContext;
|
||||||
|
use App\Framework\Filesystem\PermissionChecker;
|
||||||
|
use App\Framework\Filesystem\ValueObjects\FileOwnership;
|
||||||
|
use App\Framework\Filesystem\ValueObjects\ProcessUser;
|
||||||
|
|
||||||
final class FilePermissionException extends FilesystemException
|
final class FilePermissionException extends FilesystemException
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
string $path,
|
string $path,
|
||||||
string $operation = 'access',
|
string $operation = 'access',
|
||||||
?string $reason = null
|
?string $reason = null,
|
||||||
|
?array $diagnosticInfo = null,
|
||||||
|
?ProcessUser $currentUser = null,
|
||||||
|
?FileOwnership $fileOwnership = null,
|
||||||
|
?FileOwnership $parentOwnership = null
|
||||||
) {
|
) {
|
||||||
$message = "Permission denied for {$operation} on file: {$path}";
|
$message = $this->buildMessage($path, $operation, $reason, $diagnosticInfo, $currentUser, $fileOwnership, $parentOwnership);
|
||||||
|
|
||||||
if ($reason) {
|
|
||||||
$message .= " ({$reason})";
|
|
||||||
}
|
|
||||||
|
|
||||||
$context = ExceptionContext::forOperation('file.permission', 'filesystem')
|
$context = ExceptionContext::forOperation('file.permission', 'filesystem')
|
||||||
->withData([
|
->withData([
|
||||||
'path' => $path,
|
'path' => $path,
|
||||||
'operation' => $operation,
|
'operation' => $operation,
|
||||||
'reason' => $reason,
|
'reason' => $reason,
|
||||||
|
])
|
||||||
|
->withDebug([
|
||||||
|
'file_permissions' => $diagnosticInfo['permissions'] ?? null,
|
||||||
|
'file_owner' => $diagnosticInfo['owner'] ?? null,
|
||||||
|
'file_group' => $diagnosticInfo['group'] ?? null,
|
||||||
|
'file_exists' => $diagnosticInfo['exists'] ?? null,
|
||||||
|
'is_file' => $diagnosticInfo['is_file'] ?? null,
|
||||||
|
'is_dir' => $diagnosticInfo['is_dir'] ?? null,
|
||||||
|
'parent_dir' => $diagnosticInfo['parent_dir'] ?? null,
|
||||||
|
'parent_writable' => $diagnosticInfo['parent_writable'] ?? null,
|
||||||
|
])
|
||||||
|
->withMetadata([
|
||||||
|
'current_user' => $currentUser?->toArray(),
|
||||||
|
'file_ownership' => $fileOwnership?->toArray(),
|
||||||
|
'parent_ownership' => $parentOwnership?->toArray(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
parent::__construct(
|
parent::__construct(
|
||||||
@@ -34,23 +52,147 @@ final class FilePermissionException extends FilesystemException
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function read(string $path, ?string $reason = null): self
|
/**
|
||||||
|
* Build detailed error message with permission and user information
|
||||||
|
*/
|
||||||
|
private function buildMessage(
|
||||||
|
string $path,
|
||||||
|
string $operation,
|
||||||
|
?string $reason,
|
||||||
|
?array $diagnosticInfo,
|
||||||
|
?ProcessUser $currentUser,
|
||||||
|
?FileOwnership $fileOwnership,
|
||||||
|
?FileOwnership $parentOwnership
|
||||||
|
): string {
|
||||||
|
$message = "Permission denied for {$operation} on file: {$path}";
|
||||||
|
|
||||||
|
if ($reason) {
|
||||||
|
$message .= " ({$reason})";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file ownership information
|
||||||
|
if ($fileOwnership !== null) {
|
||||||
|
$message .= "\n File owner: {$fileOwnership->owner}, group: {$fileOwnership->group}";
|
||||||
|
} elseif (isset($diagnosticInfo['owner'], $diagnosticInfo['group'])) {
|
||||||
|
$message .= sprintf(
|
||||||
|
"\n File owner: %s, group: %s",
|
||||||
|
$diagnosticInfo['owner'],
|
||||||
|
$diagnosticInfo['group']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file permissions
|
||||||
|
if (isset($diagnosticInfo['permissions'])) {
|
||||||
|
$message .= ", permissions: {$diagnosticInfo['permissions']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add parent directory information
|
||||||
|
if (isset($diagnosticInfo['parent_dir'])) {
|
||||||
|
$parentInfo = [];
|
||||||
|
if ($parentOwnership !== null) {
|
||||||
|
$parentInfo[] = "owner: {$parentOwnership->owner}";
|
||||||
|
$parentInfo[] = "group: {$parentOwnership->group}";
|
||||||
|
}
|
||||||
|
if (isset($diagnosticInfo['parent_writable'])) {
|
||||||
|
$parentInfo[] = 'writable: ' . ($diagnosticInfo['parent_writable'] ? 'yes' : 'no');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($parentInfo)) {
|
||||||
|
$message .= "\n Parent directory: {$diagnosticInfo['parent_dir']} (" . implode(', ', $parentInfo) . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current process user information
|
||||||
|
if ($currentUser !== null) {
|
||||||
|
$message .= "\n Current process user: {$currentUser->toString()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create exception with detailed permission information
|
||||||
|
*/
|
||||||
|
private static function createWithDetails(
|
||||||
|
string $path,
|
||||||
|
string $operation,
|
||||||
|
?string $reason = null,
|
||||||
|
?PermissionChecker $permissionChecker = null
|
||||||
|
): self {
|
||||||
|
$diagnosticInfo = null;
|
||||||
|
$fileOwnership = null;
|
||||||
|
$parentOwnership = null;
|
||||||
|
|
||||||
|
if ($permissionChecker !== null) {
|
||||||
|
try {
|
||||||
|
$diagnosticInfo = $permissionChecker->getDiagnosticInfo($path);
|
||||||
|
// Use resolved_path from diagnostic info if available
|
||||||
|
$resolvedPath = $diagnosticInfo['resolved_path'] ?? $path;
|
||||||
|
|
||||||
|
// Get file ownership
|
||||||
|
if (file_exists($resolvedPath)) {
|
||||||
|
$fileOwnership = FileOwnership::fromPath($resolvedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent directory ownership
|
||||||
|
if (isset($diagnosticInfo['parent_dir'])) {
|
||||||
|
$parentDir = $diagnosticInfo['parent_dir'];
|
||||||
|
if (file_exists($parentDir)) {
|
||||||
|
$parentOwnership = FileOwnership::fromPath($parentDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// If diagnostic info collection fails, continue without it
|
||||||
|
$diagnosticInfo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentUser = ProcessUser::current();
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
path: $path,
|
||||||
|
operation: $operation,
|
||||||
|
reason: $reason,
|
||||||
|
diagnosticInfo: $diagnosticInfo,
|
||||||
|
currentUser: $currentUser,
|
||||||
|
fileOwnership: $fileOwnership,
|
||||||
|
parentOwnership: $parentOwnership
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function read(string $path, ?string $reason = null, ?PermissionChecker $permissionChecker = null): self
|
||||||
{
|
{
|
||||||
|
if ($permissionChecker !== null) {
|
||||||
|
return self::createWithDetails($path, 'read', $reason, $permissionChecker);
|
||||||
|
}
|
||||||
|
|
||||||
return new self($path, 'read', $reason);
|
return new self($path, 'read', $reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function write(string $path, ?string $reason = null): self
|
public static function write(string $path, ?string $reason = null, ?PermissionChecker $permissionChecker = null): self
|
||||||
{
|
{
|
||||||
|
if ($permissionChecker !== null) {
|
||||||
|
return self::createWithDetails($path, 'write', $reason, $permissionChecker);
|
||||||
|
}
|
||||||
|
|
||||||
return new self($path, 'write', $reason);
|
return new self($path, 'write', $reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function delete(string $path, ?string $reason = null): self
|
public static function delete(string $path, ?string $reason = null, ?PermissionChecker $permissionChecker = null): self
|
||||||
{
|
{
|
||||||
|
if ($permissionChecker !== null) {
|
||||||
|
return self::createWithDetails($path, 'delete', $reason, $permissionChecker);
|
||||||
|
}
|
||||||
|
|
||||||
return new self($path, 'delete', $reason);
|
return new self($path, 'delete', $reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function createDirectory(string $path, ?string $reason = null): self
|
public static function createDirectory(string $path, ?string $reason = null, ?PermissionChecker $permissionChecker = null): self
|
||||||
{
|
{
|
||||||
|
if ($permissionChecker !== null) {
|
||||||
|
return self::createWithDetails($path, 'create directory', $reason, $permissionChecker);
|
||||||
|
}
|
||||||
|
|
||||||
return new self($path, 'create directory', $reason);
|
return new self($path, 'create directory', $reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,14 +115,14 @@ final readonly class FileStorage implements Storage, AtomicStorage, AppendableSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (! is_readable($resolvedPath)) {
|
if (! is_readable($resolvedPath)) {
|
||||||
throw FilePermissionException::read($path, 'File is not readable');
|
throw FilePermissionException::read($path, 'File is not readable', $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = @file_get_contents($resolvedPath);
|
$content = @file_get_contents($resolvedPath);
|
||||||
if ($content === false) {
|
if ($content === false) {
|
||||||
$error = error_get_last();
|
$error = error_get_last();
|
||||||
if ($error && str_contains($error['message'], 'Permission denied')) {
|
if ($error && str_contains($error['message'], 'Permission denied')) {
|
||||||
throw FilePermissionException::read($path, $error['message']);
|
throw FilePermissionException::read($path, $error['message'], $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FileReadException($path);
|
throw new FileReadException($path);
|
||||||
@@ -156,24 +156,24 @@ final readonly class FileStorage implements Storage, AtomicStorage, AppendableSt
|
|||||||
if (! @mkdir($dir, 0777, true) && ! is_dir($dir)) {
|
if (! @mkdir($dir, 0777, true) && ! is_dir($dir)) {
|
||||||
$error = error_get_last();
|
$error = error_get_last();
|
||||||
if ($error && str_contains($error['message'], 'Permission denied')) {
|
if ($error && str_contains($error['message'], 'Permission denied')) {
|
||||||
throw FilePermissionException::createDirectory($dir, $error['message']);
|
throw FilePermissionException::createDirectory($dir, $error['message'], $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DirectoryCreateException($dir);
|
throw new DirectoryCreateException($dir);
|
||||||
}
|
}
|
||||||
} elseif (! is_writable($dir)) {
|
} elseif (! is_writable($dir)) {
|
||||||
throw FilePermissionException::write($path, 'Directory is not writable: ' . $dir);
|
throw FilePermissionException::write($path, 'Directory is not writable: ' . $dir, $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe File-Permissions wenn Datei bereits existiert
|
// Prüfe File-Permissions wenn Datei bereits existiert
|
||||||
if (is_file($resolvedPath) && ! is_writable($resolvedPath)) {
|
if (is_file($resolvedPath) && ! is_writable($resolvedPath)) {
|
||||||
throw FilePermissionException::write($path, 'File is not writable');
|
throw FilePermissionException::write($path, 'File is not writable', $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (@file_put_contents($resolvedPath, $content) === false) {
|
if (@file_put_contents($resolvedPath, $content) === false) {
|
||||||
$error = error_get_last();
|
$error = error_get_last();
|
||||||
if ($error && str_contains($error['message'], 'Permission denied')) {
|
if ($error && str_contains($error['message'], 'Permission denied')) {
|
||||||
throw FilePermissionException::write($path, $error['message']);
|
throw FilePermissionException::write($path, $error['message'], $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FileWriteException($path);
|
throw new FileWriteException($path);
|
||||||
@@ -229,7 +229,7 @@ final readonly class FileStorage implements Storage, AtomicStorage, AppendableSt
|
|||||||
if (! @unlink($resolvedPath)) {
|
if (! @unlink($resolvedPath)) {
|
||||||
$error = error_get_last();
|
$error = error_get_last();
|
||||||
if ($error && str_contains($error['message'], 'Permission denied')) {
|
if ($error && str_contains($error['message'], 'Permission denied')) {
|
||||||
throw FilePermissionException::delete($path, $error['message']);
|
throw FilePermissionException::delete($path, $error['message'], $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FileDeleteException($path);
|
throw new FileDeleteException($path);
|
||||||
@@ -386,13 +386,13 @@ final readonly class FileStorage implements Storage, AtomicStorage, AppendableSt
|
|||||||
// Prüfe Parent-Directory Permissions
|
// Prüfe Parent-Directory Permissions
|
||||||
$parentDir = dirname($resolvedPath);
|
$parentDir = dirname($resolvedPath);
|
||||||
if (is_dir($parentDir) && ! is_writable($parentDir)) {
|
if (is_dir($parentDir) && ! is_writable($parentDir)) {
|
||||||
throw FilePermissionException::createDirectory($path, 'Parent directory is not writable: ' . $parentDir);
|
throw FilePermissionException::createDirectory($path, 'Parent directory is not writable: ' . $parentDir, $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! @mkdir($resolvedPath, $permissions, $recursive) && ! is_dir($resolvedPath)) {
|
if (! @mkdir($resolvedPath, $permissions, $recursive) && ! is_dir($resolvedPath)) {
|
||||||
$error = error_get_last();
|
$error = error_get_last();
|
||||||
if ($error && str_contains($error['message'], 'Permission denied')) {
|
if ($error && str_contains($error['message'], 'Permission denied')) {
|
||||||
throw FilePermissionException::createDirectory($path, $error['message']);
|
throw FilePermissionException::createDirectory($path, $error['message'], $this->permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DirectoryCreateException($path);
|
throw new DirectoryCreateException($path);
|
||||||
|
|||||||
116
src/Framework/Filesystem/ValueObjects/FileOwnership.php
Normal file
116
src/Framework/Filesystem/ValueObjects/FileOwnership.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Filesystem\ValueObjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object representing file ownership (owner and group)
|
||||||
|
*/
|
||||||
|
final readonly class FileOwnership
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $owner,
|
||||||
|
public string $group,
|
||||||
|
public int $ownerUid,
|
||||||
|
public int $groupGid
|
||||||
|
) {
|
||||||
|
if (empty($owner)) {
|
||||||
|
throw new \InvalidArgumentException("Owner name cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($group)) {
|
||||||
|
throw new \InvalidArgumentException("Group name cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ownerUid < 0) {
|
||||||
|
throw new \InvalidArgumentException("Invalid owner UID: {$ownerUid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($groupGid < 0) {
|
||||||
|
throw new \InvalidArgumentException("Invalid group GID: {$groupGid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file ownership from a file path
|
||||||
|
*
|
||||||
|
* @return self|null Returns null if file doesn't exist or posix functions are not available
|
||||||
|
*/
|
||||||
|
public static function fromPath(string $path): ?self
|
||||||
|
{
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('fileowner') || !function_exists('filegroup')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ownerUid = fileowner($path);
|
||||||
|
$groupGid = filegroup($path);
|
||||||
|
|
||||||
|
if ($ownerUid === false || $groupGid === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInfo = posix_getpwuid($ownerUid);
|
||||||
|
$groupInfo = posix_getgrgid($groupGid);
|
||||||
|
|
||||||
|
if ($userInfo === false || $groupInfo === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
owner: $userInfo['name'] ?? 'unknown',
|
||||||
|
group: $groupInfo['name'] ?? 'unknown',
|
||||||
|
ownerUid: $ownerUid,
|
||||||
|
groupGid: $groupGid
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if ownership matches a process user
|
||||||
|
*/
|
||||||
|
public function matchesProcessUser(?ProcessUser $processUser): bool
|
||||||
|
{
|
||||||
|
if ($processUser === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->ownerUid === $processUser->uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if ownership matches by user name
|
||||||
|
*/
|
||||||
|
public function matchesOwner(string $userName): bool
|
||||||
|
{
|
||||||
|
return $this->owner === $userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get string representation
|
||||||
|
*/
|
||||||
|
public function toString(): string
|
||||||
|
{
|
||||||
|
return "{$this->owner}:{$this->group} (uid: {$this->ownerUid}, gid: {$this->groupGid})";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to array for logging/debugging
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'owner' => $this->owner,
|
||||||
|
'group' => $this->group,
|
||||||
|
'owner_uid' => $this->ownerUid,
|
||||||
|
'group_gid' => $this->groupGid,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/Framework/Filesystem/ValueObjects/ProcessUser.php
Normal file
108
src/Framework/Filesystem/ValueObjects/ProcessUser.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Framework\Filesystem\ValueObjects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value Object representing the current process user
|
||||||
|
*/
|
||||||
|
final readonly class ProcessUser
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $uid,
|
||||||
|
public int $gid,
|
||||||
|
public string $name,
|
||||||
|
public string $group
|
||||||
|
) {
|
||||||
|
if ($uid < 0) {
|
||||||
|
throw new \InvalidArgumentException("Invalid user ID: {$uid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gid < 0) {
|
||||||
|
throw new \InvalidArgumentException("Invalid group ID: {$gid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
throw new \InvalidArgumentException("User name cannot be empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current process user from system
|
||||||
|
*
|
||||||
|
* @return self|null Returns null if posix functions are not available
|
||||||
|
*/
|
||||||
|
public static function current(): ?self
|
||||||
|
{
|
||||||
|
if (!function_exists('posix_geteuid')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$uid = posix_geteuid();
|
||||||
|
$gid = posix_getegid();
|
||||||
|
$userInfo = posix_getpwuid($uid);
|
||||||
|
$groupInfo = posix_getgrgid($gid);
|
||||||
|
|
||||||
|
if ($userInfo === false || $groupInfo === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
uid: $uid,
|
||||||
|
gid: $gid,
|
||||||
|
name: $userInfo['name'] ?? 'unknown',
|
||||||
|
group: $groupInfo['name'] ?? 'unknown'
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// posix functions failed - return null
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this user is root (uid 0)
|
||||||
|
*/
|
||||||
|
public function isRoot(): bool
|
||||||
|
{
|
||||||
|
return $this->uid === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this user matches another user by name
|
||||||
|
*/
|
||||||
|
public function matchesName(string $userName): bool
|
||||||
|
{
|
||||||
|
return $this->name === $userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this user matches another user by UID
|
||||||
|
*/
|
||||||
|
public function matchesUid(int $uid): bool
|
||||||
|
{
|
||||||
|
return $this->uid === $uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get string representation
|
||||||
|
*/
|
||||||
|
public function toString(): string
|
||||||
|
{
|
||||||
|
return "{$this->name} (uid: {$this->uid}, gid: {$this->gid}, group: {$this->group})";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to array for logging/debugging
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'uid' => $this->uid,
|
||||||
|
'gid' => $this->gid,
|
||||||
|
'name' => $this->name,
|
||||||
|
'group' => $this->group,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user