mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
Audit review (rounds 1-31): fix confirmed issues, remove dead code, harden tests
Security/correctness fixes: - fail-open token validation preserved with lenient-audience; access token that fails verification and has no ID token to corroborate now fails closed - token_manager blacklist-expiry: fix impossible tautology (x>24h && x<24h) - constant-time OAuth state compare; clear state/nonce/code_verifier on callback token-exchange failure to prevent replay of half-failed flows - StopAllTasks gated on last-instance close - background-task panic recovery; remove task-registry counter leak - circuit breaker half-open: release probe slot; CB + per-session outcome recorded once per refresh operation, not once per waiter - Redis connection pool: handle nil receive when pool closed concurrently - FileStore: atomic temp-write + rename - errors_recovery health-check data race -> atomic Test-isolation / hardening: - reset helpers for Once-pinned global cache manager and transport pool - coverage: utils 70->93% (redirect URL security contract); fuzz targets for bearer parsers; concurrent ServeHTTP race tests for the shared request path Dead-code removal (verified orphaned, build+suite green): internal/providers (30 files, 0 importers), internal/compat, internal/features, input_validation. Verified: govulncheck, go mod tidy, yaegi-validate, cross-compile (darwin+ linux), golangci-lint (new-from-rev clean), 3x full race+shuffle green.
This commit is contained in:
@@ -83,10 +83,18 @@ func (s *FileStore) Save(ctx context.Context, providerURL string, creds *ClientR
|
||||
return fmt.Errorf("failed to marshal credentials: %w", err)
|
||||
}
|
||||
|
||||
// Write with restrictive permissions (owner read/write only)
|
||||
if err := os.WriteFile(filePath, data, 0600); err != nil {
|
||||
// Write with restrictive permissions (owner read/write only). Write to a
|
||||
// temp file and atomically rename so a crash mid-write can never leave a
|
||||
// truncated credentials file (which would force an avoidable client
|
||||
// re-registration on the next start).
|
||||
tmpPath := filePath + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, data, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write credentials file: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmpPath, filePath); err != nil {
|
||||
_ = os.Remove(tmpPath) // best-effort cleanup
|
||||
return fmt.Errorf("failed to commit credentials file: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debugf("Saved client credentials to %s", filePath)
|
||||
return nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package dcrstorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -462,3 +463,69 @@ func TestFileStore_DirectoryCreation(t *testing.T) {
|
||||
t.Error("Failed to load credentials from nested directory")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileStore_AtomicWrite guards the temp-file + rename implementation:
|
||||
// after a successful Save the committed file is a valid JSON document, no
|
||||
// ".tmp" scratch file is left behind, and the file mode is 0600.
|
||||
func TestFileStore_AtomicWrite(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tempDir := t.TempDir()
|
||||
basePath := filepath.Join(tempDir, "credentials.json")
|
||||
store := NewFileStore(basePath, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
providerURL := "https://auth.example.com"
|
||||
creds := &ClientRegistrationResponse{
|
||||
ClientID: "atomic-client",
|
||||
ClientSecret: "s3cret",
|
||||
}
|
||||
|
||||
if err := store.Save(ctx, providerURL, creds); err != nil {
|
||||
t.Fatalf("Save failed: %v", err)
|
||||
}
|
||||
|
||||
filePath := store.GetFilePath(providerURL)
|
||||
|
||||
// No scratch file should remain.
|
||||
if _, err := os.Stat(filePath + ".tmp"); !os.IsNotExist(err) {
|
||||
t.Errorf("expected no %q scratch file after Save, stat err=%v", filePath+".tmp", err)
|
||||
}
|
||||
|
||||
// Committed file must be valid JSON.
|
||||
raw, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read committed file: %v", err)
|
||||
}
|
||||
var parsed ClientRegistrationResponse
|
||||
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||
t.Fatalf("committed file is not valid JSON: %v", err)
|
||||
}
|
||||
if parsed.ClientID != creds.ClientID || parsed.ClientSecret != creds.ClientSecret {
|
||||
t.Errorf("committed content mismatch: got %q/%q", parsed.ClientID, parsed.ClientSecret)
|
||||
}
|
||||
|
||||
// Mode should be owner read/write.
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat committed file: %v", err)
|
||||
}
|
||||
if info.Mode().Perm() != 0600 {
|
||||
t.Errorf("expected mode 0600, got %v", info.Mode().Perm())
|
||||
}
|
||||
|
||||
// Overwriting an existing file must also be atomic and leave no residue.
|
||||
if err := store.Save(ctx, providerURL, &ClientRegistrationResponse{ClientID: "updated"}); err != nil {
|
||||
t.Fatalf("second Save failed: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filePath + ".tmp"); !os.IsNotExist(err) {
|
||||
t.Errorf("expected no %q scratch file after overwrite", filePath+".tmp")
|
||||
}
|
||||
loaded, err := store.Load(ctx, providerURL)
|
||||
if err != nil {
|
||||
t.Fatalf("Load after overwrite failed: %v", err)
|
||||
}
|
||||
if loaded == nil || loaded.ClientID != "updated" {
|
||||
t.Errorf("overwrite not reflected: %+v", loaded)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user