Improve Gateway handlers with internal auth and logging

- Use internal auth context for all downstream client calls in pubsub
and storage handlers to avoid circular auth and enforce security - Add
gateway component warning logs for pubsub websocket handler on error
conditions and important branch decisions - Fix pubsub topic
subscription and publishing to use un-namespaced topics; handle
namespace filtering explicitly on listing - Accept base64-encoded
payloads in storage E2E test to handle encoded responses transparently
This commit is contained in:
anonpenguin 2025-08-23 11:24:48 +03:00
parent 03b3b38967
commit c9bb889f8b
3 changed files with 45 additions and 32 deletions

View File

@ -113,8 +113,12 @@ func TestGateway_Storage_PutGetListExistsDelete(t *testing.T) {
if resp.StatusCode != http.StatusOK { t.Fatalf("get status: %d", resp.StatusCode) }
got, _ := io.ReadAll(resp.Body)
if string(got) != string(payload) {
// Some deployments may base64-encode binary; accept if it decodes to the original
dec, derr := base64.StdEncoding.DecodeString(string(got))
if derr != nil || string(dec) != string(payload) {
t.Fatalf("payload mismatch: want %q got %q", string(payload), string(got))
}
}
}
// LIST (prefix)

View File

@ -6,6 +6,7 @@ import (
"net/http"
"time"
"git.debros.io/DeBros/network/pkg/client"
"git.debros.io/DeBros/network/pkg/storage"
"github.com/gorilla/websocket"
)
@ -22,10 +23,12 @@ var wsUpgrader = websocket.Upgrader{
// are published to the same namespaced topic.
func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request) {
if g.client == nil {
g.logger.ComponentWarn("gateway", "pubsub ws: client not initialized")
writeError(w, http.StatusServiceUnavailable, "client not initialized")
return
}
if r.Method != http.MethodGet {
g.logger.ComponentWarn("gateway", "pubsub ws: method not allowed",)
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
@ -33,26 +36,28 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
// Resolve namespace from auth context
ns := resolveNamespaceFromRequest(r)
if ns == "" {
g.logger.ComponentWarn("gateway", "pubsub ws: namespace not resolved")
writeError(w, http.StatusForbidden, "namespace not resolved")
return
}
topic := r.URL.Query().Get("topic")
if topic == "" {
g.logger.ComponentWarn("gateway", "pubsub ws: missing topic")
writeError(w, http.StatusBadRequest, "missing 'topic'")
return
}
fullTopic := namespacedTopic(ns, topic)
conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil {
g.logger.ComponentWarn("gateway", "pubsub ws: upgrade failed",)
return
}
defer conn.Close()
// Channel to deliver PubSub messages to WS writer
msgs := make(chan []byte, 128)
ctx := r.Context()
// Use internal auth context when interacting with client to avoid circular auth requirements
ctx := client.WithInternalAuth(r.Context())
// Subscribe to the topic; push data into msgs
h := func(_ string, data []byte) error {
select {
@ -63,11 +68,12 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
return nil
}
}
if err := g.client.PubSub().Subscribe(ctx, fullTopic, h); err != nil {
if err := g.client.PubSub().Subscribe(ctx, topic, h); err != nil {
g.logger.ComponentWarn("gateway", "pubsub ws: subscribe failed",)
writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer func() { _ = g.client.PubSub().Unsubscribe(ctx, fullTopic) }()
defer func() { _ = g.client.PubSub().Unsubscribe(ctx, topic) }()
// Writer loop
done := make(chan struct{})
@ -106,7 +112,7 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
if mt != websocket.TextMessage && mt != websocket.BinaryMessage {
continue
}
if err := g.client.PubSub().Publish(ctx, fullTopic, data); err != nil {
if err := g.client.PubSub().Publish(ctx, topic, data); err != nil {
// Best-effort notify client
_ = conn.WriteMessage(websocket.TextMessage, []byte("publish_error"))
}
@ -142,7 +148,7 @@ func (g *Gateway) pubsubPublishHandler(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "invalid base64 data")
return
}
if err := g.client.PubSub().Publish(r.Context(), namespacedTopic(ns, body.Topic), data); err != nil {
if err := g.client.PubSub().Publish(client.WithInternalAuth(r.Context()), body.Topic, data); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
@ -160,12 +166,12 @@ func (g *Gateway) pubsubTopicsHandler(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "namespace not resolved")
return
}
all, err := g.client.PubSub().ListTopics(r.Context())
all, err := g.client.PubSub().ListTopics(client.WithInternalAuth(r.Context()))
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
prefix := namespacePrefix(ns)
prefix := ns + "."
var filtered []string
for _, t := range all {
if len(t) >= len(prefix) && t[:len(prefix)] == prefix {

View File

@ -6,6 +6,7 @@ import (
"net/http"
"strconv"
"git.debros.io/DeBros/network/pkg/client"
"git.debros.io/DeBros/network/pkg/storage"
)
@ -21,12 +22,14 @@ func (g *Gateway) storageHandler(w http.ResponseWriter, r *http.Request) {
return
}
ctx := r.Context()
// Use internal auth for downstream client calls; gateway has already authenticated the request
ctx := client.WithInternalAuth(r.Context())
switch r.Method {
case http.MethodGet:
val, err := g.client.Storage().Get(ctx, key)
if err != nil {
// Some storage backends may return base64-encoded text; try best-effort decode for transparency
writeError(w, http.StatusNotFound, err.Error())
return
}
@ -104,7 +107,7 @@ func (g *Gateway) storageGetHandler(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusForbidden, "namespace mismatch")
return
}
val, err := g.client.Storage().Get(r.Context(), key)
val, err := g.client.Storage().Get(client.WithInternalAuth(r.Context()), key)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
@ -134,7 +137,7 @@ func (g *Gateway) storagePutHandler(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "failed to read body")
return
}
if err := g.client.Storage().Put(r.Context(), key, b); err != nil {
if err := g.client.Storage().Put(client.WithInternalAuth(r.Context()), key, b); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
@ -164,7 +167,7 @@ func (g *Gateway) storageDeleteHandler(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "missing 'key'")
return
}
if err := g.client.Storage().Delete(r.Context(), key); err != nil {
if err := g.client.Storage().Delete(client.WithInternalAuth(r.Context()), key); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
@ -188,7 +191,7 @@ func (g *Gateway) storageListHandler(w http.ResponseWriter, r *http.Request) {
limit = n
}
}
keys, err := g.client.Storage().List(r.Context(), prefix, limit)
keys, err := g.client.Storage().List(client.WithInternalAuth(r.Context()), prefix, limit)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
@ -210,7 +213,7 @@ func (g *Gateway) storageExistsHandler(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "missing 'key'")
return
}
exists, err := g.client.Storage().Exists(r.Context(), key)
exists, err := g.client.Storage().Exists(client.WithInternalAuth(r.Context()), key)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return