ginx

package
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Bearer-token authentication middleware for Gin-based services. Routes that require a signed-in user wrap their group with AuthMiddleware; the actual token verification stays in the service layer (passed in as a function) so this shared package never has to know about JWT internals or which signing scheme a given service uses.

Package ginx contains Gin-specific helpers every HTTP service in the project shares: request binding with validator-driven 400s, typed error responses, request-logging middleware, and a route- mount collector. Shipped into <project>/common/go/ginx/ on `maestro init`.

This file holds the request-binding helper.

Failure-response shape + writer for Gin-based services. Error defines the wire schema for 4xx/5xx bodies; WriteError maps a service-layer error onto status + Error response via the APIError interface that service sentinels are expected to implement.

HTTP middleware shipped with ginx. Every service ends up needing a per-request log line; RequestLogger is the canonical shape so the team doesn't reinvent it once per service.

Route-mount collector for Gin-based services. Each handler file in a service's routes package appends its block via init(); the server calls Register once to wire everything up. Keeps the server wiring uniform regardless of which conditional handler files happen to be present (relevant for preset-driven services where filename-suffix gating determines what's on disk).

Index

Constants

View Source
const DefaultMaxBodyBytes int64 = 1 << 20

DefaultMaxBodyBytes caps a request body at 1 MiB — generous for the JSON payloads these services accept, small enough that an unbounded or hostile body can't exhaust server memory.

Variables

This section is empty.

Functions

func AuthMiddleware

func AuthMiddleware(verify func(token string) (userID, sessionID uuid.UUID, role identity.Role, err error)) gin.HandlerFunc

AuthMiddleware returns a gin middleware that authenticates the Authorization: Bearer <token> header. verify is the service-layer check, e.g. Service.VerifyAccessToken, that validates the token and returns the user id, the session id it belongs to, and the caller's staff role. Any error, such as a missing header, wrong scheme, or invalid/expired token, aborts the request with 401 and the standard Error body. On success all three are stashed for handlers to read via UserID / SessionID / Role, so RequireRole gates routes behind this middleware. A scheme with no session concept can return uuid.Nil for the session id.

func Bind

func Bind(c *gin.Context, out any) bool

Bind parses the request body into out and writes a 400 if it fails — either a malformed body or a binding-tag violation. Returns true when the handler can proceed, false when it should return immediately (Gin has already written the response).

The 400 body uses the Error shape defined in this package so the failure contract is consistent across endpoints.

Typical handler shape:

func login(c *gin.Context) {
    var req LoginRequest
    if !ginx.Bind(c, &req) {
        return
    }
    // ...
}

func BodyLimit

func BodyLimit(maxBytes int64) gin.HandlerFunc

BodyLimit wraps every request body in http.MaxBytesReader so a client can't stream an arbitrarily large (or never-ending) body to exhaust memory. When the limit is exceeded the body read fails, which Bind surfaces as a 400. Install once at engine setup, before the route handlers.

func CORS

func CORS(cfg CORSConfig) gin.HandlerFunc

CORS returns a gin middleware enforcing cfg. Mount it first in a chain so a preflight is answered before auth/rate-limit, and so CORS headers ride along on error responses (401/429/5xx) too — without them a browser hides the real status behind an opaque CORS failure.

Behavior: a request with no Origin passes through untouched (not a CORS request). A disallowed origin gets no CORS headers — a preflight is answered 204 (the browser rejects it for lack of Allow-Origin), an actual request proceeds but the browser blocks the JS read. An allowed origin gets Access-Control-Allow-Origin ("*" only when credentials are off; otherwise the concrete origin is echoed with Vary: Origin, since "*" with credentials is forbidden by the spec).

func RequestLogger

func RequestLogger() gin.HandlerFunc

RequestLogger emits one zap line per request with the standard fields (method, path, status, ms). Typed zap fields keep the hot path allocation-friendly. Install once at gin.Engine setup time alongside Recovery:

r := gin.New()
r.Use(gin.Recovery(), ginx.RequestLogger())

Logging uses zap.L() so the caller is expected to have already installed a global logger via logger.New + zap.ReplaceGlobals.

func RequireRole

func RequireRole(tier identity.Role) gin.HandlerFunc

RequireRole gates a route group on a minimum staff tier. Tiers are cumulative, so the check is role >= tier. Below the tier, or with no role on the context, the response is a 404 not a 403, so probing for admin routes looks identical to hitting routes that don't exist. It runs after whatever middleware authenticated the caller and stashed the role: VerifyGateway for services behind the gateway, AuthMiddleware once it threads the role claim.

func Role

func Role(c *gin.Context) (identity.Role, bool)

Role returns the authenticated caller's staff tier. ok is false when no auth middleware ran.

func SecurityHeaders

func SecurityHeaders() gin.HandlerFunc

SecurityHeaders sets a baseline of defensive response headers on every response. These are cheap, broadly-safe defaults for a JSON API; HSTS and a Content-Security-Policy are left to the TLS-terminating edge, since they depend on the deployment's HTTPS and asset topology. Install at engine setup.

func SessionID

func SessionID(c *gin.Context) (uuid.UUID, bool)

SessionID returns the session id (the access token's session family) AuthMiddleware stashed on the context. It may be uuid.Nil if the auth scheme carries no session concept. ok is false only when no AuthMiddleware ran on the route.

func UserID

func UserID(c *gin.Context) (uuid.UUID, bool)

UserID returns the authenticated user id AuthMiddleware stashed on the context. ok is false only when no AuthMiddleware ran on the route — a wiring bug — so handlers mounted behind AuthMiddleware can treat a false return as "impossible" and 500.

func VerifyGateway

func VerifyGateway(verify func(token string) (subject string, role identity.Role, ok bool)) gin.HandlerFunc

VerifyGateway returns middleware that authenticates the gateway's signed assertion before a service trusts the caller. verify validates the assertion and returns the asserted subject (the user id, or "" for a public request), the caller's staff role, or ok=false to reject. On success the middleware:

  • replaces any inbound X-User-Id with the verified subject, so X-User-Id is only ever the value cryptographically asserted by the gateway;
  • replaces any inbound X-User-Role with the verified role (read via identity.RoleOf) and stashes the same role on the gin context, so RequireRole works for a service behind the gateway exactly as it does behind AuthMiddleware; and
  • stashes the raw assertion on the request context (identity.WithGatewayToken) so the handler's own service-to-service calls can forward it.

/health and /ready bypass since orchestrator probes hit them directly, not through the gateway.

A nil verify DISABLES the pass-through check with a loud warning, so the control can roll out before keys are wired everywhere. In that mode the gateway falls back to injecting a raw X-User-Id and services trust it as before and no role reaches the context, so RequireRole fails closed (admin routes 404 for everyone) until keys are wired. Configure keys on the gateway AND every service together to enforce.

func WriteError

func WriteError(c *gin.Context, err error)

WriteError translates err into the right HTTP status + Error body. Errors that satisfy APIError carry their own status + code; everything else falls through to 500 with a generic code so implementation detail (e.g. a wrapped SQL error) doesn't leak through the API.

Types

type APIError

type APIError interface {
	error
	StatusCode() int
	Code() string
}

APIError is the contract a service sentinel must satisfy for WriteError to translate it into a typed HTTP response. Each sentinel knows its own HTTP status and stable error code — the HTTP layer doesn't hardcode a sentinel list.

Example sentinel definition in a service package:

type sentinelError struct {
    status int
    code   string
    msg    string
}

func (e *sentinelError) Error() string   { return e.msg }
func (e *sentinelError) StatusCode() int { return e.status }
func (e *sentinelError) Code() string    { return e.code }

type CORSConfig

type CORSConfig struct {
	AllowedOrigins   []string
	AllowedMethods   []string
	AllowedHeaders   []string
	AllowCredentials bool
	MaxAge           int // preflight cache, seconds
}

CORSConfig configures the CORS middleware. AllowedOrigins is the only field that gates anything: an empty list means the middleware adds no CORS headers (callers that want to skip CORS entirely should just not mount it). "*" allows any origin. Methods/Headers default to a reasonable set when left empty.

func (CORSConfig) Validate

func (c CORSConfig) Validate() error

Validate rejects an unsafe CORS configuration. A wildcard origin combined with credentials is forbidden by the CORS spec for literal "*"; reflecting the request origin instead (as the middleware would) silently bypasses that protection and lets ANY site make credentialed cross-origin requests. Callers should run this at startup and fail loud so an operator can't enable it by accident — list explicit origins when credentials are on. Any origin containing "*" is rejected (not just the literal "*"): a real CORS origin is a concrete scheme://host[:port], so a "*" anywhere is either the wildcard or a wildcard-pattern the middleware can't honour — both are misconfigurations.

type Error

type Error struct {
	// Code is a stable, machine-readable identifier. Values come
	// from the service layer's sentinel errors (via APIError below)
	// or from validator-driven 400s ("validation_failed").
	Code string `json:"error"`

	// Details is human-readable and optional. Useful for surfacing
	// the validator's "field 'email' is not a valid address" in
	// development; safe to suppress in production via an env toggle.
	Details string `json:"details,omitempty"`
}

Error is the JSON shape returned for 4xx/5xx failures. The struct field is Code (not Error) to avoid a self-shadowed name inside the type definition; the JSON tag keeps the wire shape at {"error": "..."} so consumers see one consistent error envelope.

type Routes

type Routes struct {
	// contains filtered or unexported fields
}

Routes collects route-mounting closures and applies them to a gin.Engine in registration order. Use one instance per Go package that owns a related set of routes — typically at the package level:

// service/internal/routes/routes.go
package routes

var Mounts ginx.Routes

// service/internal/routes/auth.go
func init() {
    Mounts.Add(func(r *gin.Engine) {
        r.POST("/auth/login", login)
    })
}

// service/internal/server/server.go
routes.Mounts.Register(engine)

func (*Routes) Add

func (r *Routes) Add(fn func(*gin.Engine))

Add appends a mount function. Typically called from init() in each file that owns a route group, so server.New doesn't need to know which handler files are present.

func (*Routes) Register

func (r *Routes) Register(engine *gin.Engine)

Register applies every collected closure to engine in registration order. Call once from server.New after base routes (/health, /ready) are wired so feature routes appear last in the table.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL