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:
shankar0123
2026-03-27 22:28:54 -04:00
parent 305c7dc851
commit de9264baf7
19 changed files with 2857 additions and 470 deletions
+38
View File
@@ -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)
})
}
}
+179
View File
@@ -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)
}
}