diff --git a/default/skills/production-readiness-audit/dimensions/structure.md b/default/skills/production-readiness-audit/dimensions/structure.md index d0adc9e1..de404994 100644 --- a/default/skills/production-readiness-audit/dimensions/structure.md +++ b/default/skills/production-readiness-audit/dimensions/structure.md @@ -96,6 +96,7 @@ return libHTTP.OK(c, pagination) ``` ``` + ### Agent 2: Error Framework Auditor ```prompt @@ -275,9 +276,9 @@ func RegisterRoutes(protected func(resource, action string) fiber.Router, handle if handler == nil { return errors.New("handler is nil") } - protected("resource", "create").Post("/v1/resources", handler.Create) - protected("resource", "read").Get("/v1/resources", handler.List) - protected("resource", "read").Get("/v1/resources/:id", handler.Get) + protected("resources", "create").Post("/v1/resources", handler.Create) + protected("resources", "read").Get("/v1/resources", handler.List) + protected("resources", "read").Get("/v1/resources/:id", handler.Get) return nil } @@ -295,13 +296,16 @@ func NewHandler(deps ...interface{}) (*Handler, error) { 2. (HARD GATE) Centralized route registration per module 3. Handler constructors validate all dependencies 4. Consistent URL patterns (v1, kebab-case, plural resources) per Ring conventions -5. All routes use protected() wrapper (no public endpoints without explicit exemption) -6. Clear separation: routes.go vs handlers.go per Ring directory structure +5. Authorization resource names are plural and match the route collection (for example, `/v1/resources` -> `protected("resources", ...)`) +6. Singular authorization resource names only appear for protected singleton/capability endpoints, with a local comment explaining the exception +7. All routes use protected() wrapper (no public endpoints without explicit exemption) +8. Clear separation: routes.go vs handlers.go per Ring directory structure **Severity Ratings:** - CRITICAL: Unprotected routes (missing auth middleware) - CRITICAL: HARD GATE violation — project does not follow hexagonal architecture per Ring standards - HIGH: Scattered route definitions +- HIGH: Authorization resource name is singular for a collection route - MEDIUM: Handler accepts nil dependencies - LOW: Inconsistent URL naming conventions @@ -1453,4 +1457,3 @@ async function fetchData(url: string): Promise { 1. ... ``` ``` - diff --git a/dev-team/docs/standards/golang/multi-tenant.md b/dev-team/docs/standards/golang/multi-tenant.md index 451bb8ae..4e15f2ed 100644 --- a/dev-team/docs/standards/golang/multi-tenant.md +++ b/dev-team/docs/standards/golang/multi-tenant.md @@ -1670,7 +1670,7 @@ MULTI_TENANT_ENABLED=true MULTI_TENANT_URL=http://dispatch layer:4003 go test ./ ```go // ❌ WRONG: Tenant middleware runs before auth on ALL routes app.Use(tenantMid.WithTenantDB) // Runs first — calls TM API before auth validates JWT -app.Post("/v1/resources", auth.Authorize("app", "resource", "post"), handler.Create) +app.Post("/v1/resources", auth.Authorize("app", "resources", "post"), handler.Create) ``` In this pattern, `WithTenantDB` executes for **every request** before `auth.Authorize` validates the JWT. A request with a forged JWT containing `tenantId: "victim-tenant"` triggers a full Tenant Manager resolution — fetching credentials, opening connections — before auth rejects it. @@ -1700,11 +1700,13 @@ func WhenEnabled(middleware fiber.Handler) fiber.Handler { ```go // ✅ CORRECT: Auth validates JWT FIRST, then tenant resolves DB // ttHandler is nil when MULTI_TENANT_ENABLED=false (single-tenant passthrough) -f.Post("/v1/resources", auth.Authorize("app", "resource", "post"), WhenEnabled(ttHandler), handler.Create) -f.Get("/v1/resources", auth.Authorize("app", "resource", "get"), WhenEnabled(ttHandler), handler.GetAll) -f.Get("/v1/resources/:id", auth.Authorize("app", "resource", "get"), WhenEnabled(ttHandler), handler.GetByID) +f.Post("/v1/resources", auth.Authorize("app", "resources", "post"), WhenEnabled(ttHandler), handler.Create) +f.Get("/v1/resources", auth.Authorize("app", "resources", "get"), WhenEnabled(ttHandler), handler.GetAll) +f.Get("/v1/resources/:id", auth.Authorize("app", "resources", "get"), WhenEnabled(ttHandler), handler.GetByID) ``` +`auth.Authorize(..., resource, ...)` MUST use the plural resource name that matches the collection route. Singular resources are allowed only for protected singleton/capability endpoints that are not collection routes, and the exception must be documented next to the route. + **How it works:** 1. `auth.Authorize(...)` is the first handler — validates JWT before anything else 2. `WhenEnabled(ttHandler)` runs second — if `ttHandler` is nil (single-tenant mode), it calls `c.Next()` immediately; if non-nil, it executes the tenant middleware @@ -2675,4 +2677,4 @@ The service catalog enforces a maximum of 2 active keys per environment, so both |-----------------|----------------|-----------------| | "We don't need API key auth for internal services" | The `/settings` endpoint returns database credentials. Unauthenticated access is a security risk. | **MUST configure `WithServiceAPIKey`** | | "We'll add the API key later" | Without authentication, the Tenant Manager rejects `/settings` requests. The service cannot resolve tenant connections. | **MUST configure before enabling multi-tenant** | -| "We can use a shared API key across services" | Each service MUST have its own API key for audit trail and independent revocation. | **MUST generate per-service keys via service catalog** | \ No newline at end of file +| "We can use a shared API key across services" | Each service MUST have its own API key for audit trail and independent revocation. | **MUST generate per-service keys via service catalog** | diff --git a/dev-team/docs/standards/golang/security.md b/dev-team/docs/standards/golang/security.md index 8c62e1f2..8eddcfee 100644 --- a/dev-team/docs/standards/golang/security.md +++ b/dev-team/docs/standards/golang/security.md @@ -6,6 +6,7 @@ This module covers authentication, licensing, and secret protection. --- + ## Table of Contents | # | Section | Description | @@ -171,6 +172,10 @@ auth.Authorize(applicationName, resource, action) | `resource` | string | Resource being accessed | `"ledgers"`, `"transactions"`, `"packages"` | | `action` | string | HTTP method (lowercase) | `"get"`, `"post"`, `"patch"`, `"delete"` | +**Resource naming rule:** `resource` MUST be plural and MUST match the REST collection name used by the route. For example, `POST /v1/payments` authorizes `"payments"`, not `"payment"`; `GET /v1/boletos/:id` authorizes `"boletos"`, not `"boleto"`. + +**Allowed exception:** use a singular resource only when the endpoint protects a singleton or capability that is not modeled as a collection route (for example, `/version` if it ever becomes protected). Document the exception next to the route registration and in the PR description. + ### Middleware Behavior | Scenario | HTTP Response | @@ -272,13 +277,16 @@ req.Header.Set("Authorization", "Bearer hardcoded-token-here") // never f.Post("/v1/sensitive-data", handler.Create) // Missing auth.Authorize // FORBIDDEN: Using wrong application name -auth.Authorize("wrong-app-name", "resource", "post") // Must match identity registration +auth.Authorize("wrong-app-name", "resources", "post") // Must match identity registration + +// FORBIDDEN: Singular resource for a collection route +auth.Authorize(applicationName, "payment", "post") // Route is /v1/payments, resource must be "payments" // FORBIDDEN: Direct calls to plugin-auth API http.Post("http://plugin-auth:4000/v1/authorize", ...) // Use lib-auth instead // CORRECT: Always use lib-auth for auth operations -auth.Authorize(applicationName, "resource", "post") +auth.Authorize(applicationName, "resources", "post") token, _ := auth.GetApplicationToken(ctx, clientID, clientSecret) ``` @@ -1952,4 +1960,3 @@ If any checkbox is unchecked → FIX before submitting. ``` --- -