mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 14:51:30 +00:00
docs: synchronize project documentation with codebase
Implements 3 deferred security tickets (TICKET-003, TICKET-007, TICKET-010) and performs comprehensive documentation audit to eliminate drift between code and docs. Code changes: - TICKET-003: Repository integration tests with testcontainers-go (50+ subtests) - TICKET-007: CertificateService decomposition into RevocationSvc + CAOperationsSvc - TICKET-010: Request body size limits via http.MaxBytesReader middleware - Fix missing slog import in certificate.go after service decomposition Documentation updates: - README: Fix endpoint count (97→93), expand env var reference (15→39 vars) - CLAUDE.md: Fix OpenAPI operation count (85→93), update file locations - architecture.md: Add body size limits section, middleware chain ordering - CONTRIBUTING.md: New contributor guide with architecture conventions, test patterns, middleware ordering, CI thresholds - SECURITY_REMEDIATION.md: Removed from repo (moved to cowork, gitignored) - Test files: Add doc comments to all new test files Documentation that should exist but doesn't yet: - Architecture diagrams (C4 model or similar) - Threat model document - Testing philosophy guide - Disaster recovery runbook - Upgrade guide (migration between versions) - API versioning strategy document Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// BodyLimitConfig holds configuration for the body size limit middleware.
|
||||
type BodyLimitConfig struct {
|
||||
MaxBytes int64 // Maximum request body size in bytes; 0 = use default (1MB)
|
||||
}
|
||||
|
||||
// DefaultMaxBodySize is the default maximum request body size (1MB).
|
||||
const DefaultMaxBodySize int64 = 1 * 1024 * 1024
|
||||
|
||||
// NewBodyLimit creates a middleware that limits request body size.
|
||||
// If the body exceeds the configured limit, the server returns 413 Request Entity Too Large.
|
||||
// This prevents clients from sending excessively large payloads that could cause
|
||||
// memory exhaustion or denial of service (CWE-400).
|
||||
func NewBodyLimit(cfg BodyLimitConfig) func(http.Handler) http.Handler {
|
||||
maxBytes := cfg.MaxBytes
|
||||
if maxBytes <= 0 {
|
||||
maxBytes = DefaultMaxBodySize
|
||||
}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip body limit for requests without bodies
|
||||
if r.Body == nil || r.ContentLength == 0 {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Wrap the body with MaxBytesReader
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Tests for the request body size limit middleware (TICKET-010).
|
||||
// Covers under/over/exact limit, nil body, default size, GET requests,
|
||||
// and custom limits.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBodyLimit_UnderLimit(t *testing.T) {
|
||||
handler := NewBodyLimit(BodyLimitConfig{MaxBytes: 1024})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected read error: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(body)
|
||||
}),
|
||||
)
|
||||
|
||||
body := bytes.NewReader([]byte("small body"))
|
||||
req := httptest.NewRequest(http.MethodPost, "/test", body)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimit_OverLimit(t *testing.T) {
|
||||
handler := NewBodyLimit(BodyLimitConfig{MaxBytes: 10})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
// MaxBytesReader returns an error when limit exceeded
|
||||
http.Error(w, `{"error":"Request body too large"}`, http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
)
|
||||
|
||||
body := bytes.NewReader([]byte("this body exceeds ten bytes"))
|
||||
req := httptest.NewRequest(http.MethodPost, "/test", body)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusRequestEntityTooLarge {
|
||||
t.Errorf("status = %d, want %d", w.Code, http.StatusRequestEntityTooLarge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimit_ExactLimit(t *testing.T) {
|
||||
data := "exactly10!" // 10 bytes
|
||||
handler := NewBodyLimit(BodyLimitConfig{MaxBytes: 10})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"Request body too large"}`, http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(body)
|
||||
}),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/test", strings.NewReader(data))
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimit_NilBody(t *testing.T) {
|
||||
handler := NewBodyLimit(BodyLimitConfig{MaxBytes: 1024})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimit_DefaultSize(t *testing.T) {
|
||||
// When MaxBytes is 0, should use default (1MB)
|
||||
mw := NewBodyLimit(BodyLimitConfig{MaxBytes: 0})
|
||||
|
||||
called := false
|
||||
handler := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
body := bytes.NewReader([]byte("test"))
|
||||
req := httptest.NewRequest(http.MethodPost, "/test", body)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if !called {
|
||||
t.Error("handler was not called")
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimit_GETRequest_NoBody(t *testing.T) {
|
||||
handler := NewBodyLimit(BodyLimitConfig{MaxBytes: 10})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimit_ContentLengthZero(t *testing.T) {
|
||||
handler := NewBodyLimit(BodyLimitConfig{MaxBytes: 10})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/test", nil)
|
||||
req.ContentLength = 0
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodyLimit_CustomMaxBytes(t *testing.T) {
|
||||
// Test with 512KB limit
|
||||
const maxSize = 512 * 1024
|
||||
handler := NewBodyLimit(BodyLimitConfig{MaxBytes: maxSize})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"Request body too large"}`, http.StatusRequestEntityTooLarge)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Length", string(rune(len(body))))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
)
|
||||
|
||||
// Create a body just under the limit
|
||||
bodyData := make([]byte, maxSize-1)
|
||||
req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader(bodyData))
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d for body just under limit", w.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user