// Package identity gates a route on the set of tailnet peers allowed to use it. // The middleware sits in front of the proxy: it names the route the same way the // proxy does and refuses with 403 any caller a route does not allow. package identity import ( "net/http" "strings" ) const ( // routeHeader is how a caller names the route, the same header the proxy // reads. routeHeader = "X-Crossbar-Route" // peerHeader carries the node name in header mode. peerHeader = "X-Crossbar-Peer" // adminPrefix is never gated here; the proxy's own handlers own it. adminPrefix = "/_crossbar/" ) // Middleware wraps next with the peer gate. peersFor names the allow list for a // route and reports whether it knows the route; an unknown route, like a path // under adminPrefix, passes straight through. func Middleware(c *Checker, peersFor func(route string) ([]string, bool), next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, adminPrefix) { next.ServeHTTP(w, r) return } route := r.Header.Get(routeHeader) if route == "" { route = firstSegment(r.URL.Path) } peers, known := peersFor(route) if !known { next.ServeHTTP(w, r) return } ctx := WithHeaderPeer(r.Context(), r.Header.Get(peerHeader)) if err := c.Allow(ctx, peers, r.RemoteAddr); err != nil { writeForbidden(w) return } next.ServeHTTP(w, r) }) } // writeForbidden answers the JSON 403 the tests and callers expect. func writeForbidden(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":"forbidden route"}`)) } // firstSegment takes the first path segment as the route, "/a/v1/x" -> "a". func firstSegment(path string) string { if path == "" || path[0] != '/' { return "" } after := path[1:] if slash := strings.IndexByte(after, '/'); slash >= 0 { if after[:slash] == "" { return "" } return after[:slash] } if after == "" { return "" } return after }