refactor(http): move gateway token to package-level auth state

Replace per-handler `token string` field with package-level
`pkgGatewayToken` via `InitGatewayToken()`, matching the existing
pattern for `pkgAPIKeyCache`, `pkgOwnerIDs`, etc.

- Remove `token` from ~30 HTTP handler structs and constructors
- Simplify `requireAuth(minRole, next)` signature (was 3 params)
- Simplify `resolveAuth(r)` signature (was 2 params)
- Rename `resolveAuthBearer` → `resolveAuthWithBearer` (clearer)
- Add `store.WithRole()`/`RoleFromContext()` to propagate caller
  role through context
- Inject role into context in `requireAuth` and `requireAuthBearer`
This commit is contained in:
viettranx committed 2026-03-23 22:57:26 +07:00
1 parent 5b749faa35
commit e1cb5c411b
37 files changed
+201 -207

No files matched your search

+17 -16
View File
@@ -289,7 +289,7 @@ func runGateway() {
server.SetPolicyEngine(permPE)
server.SetPairingService(pgStores.Pairing)
server.SetMessageBus(msgBus)
server.SetOAuthHandler(httpapi.NewOAuthHandler(cfg.Gateway.Token, pgStores.Providers, pgStores.ConfigSecrets, providerRegistry, msgBus))
server.SetOAuthHandler(httpapi.NewOAuthHandler(pgStores.Providers, pgStores.ConfigSecrets, providerRegistry, msgBus))
// contextFileInterceptor is created inside wireExtras.
// Declared here so it can be passed to registerAllMethods → AgentsMethods
@@ -313,7 +313,8 @@ func runGateway() {
if mcpMgr != nil {
mcpToolLister = mcpMgr
}
agentsH, skillsH, tracesH, mcpH, channelInstancesH, providersH, builtinToolsH, pendingMessagesH, teamEventsH, secureCLIH, mcpUserCredsH := wireHTTP(pgStores, cfg.Gateway.Token, cfg.Agents.Defaults.Workspace, dataDir, bundledSkillsDir, msgBus, toolsReg, providerRegistry, permPE.IsOwner, gatewayAddr, mcpToolLister)
httpapi.InitGatewayToken(cfg.Gateway.Token)
agentsH, skillsH, tracesH, mcpH, channelInstancesH, providersH, builtinToolsH, pendingMessagesH, teamEventsH, secureCLIH, mcpUserCredsH := wireHTTP(pgStores, cfg.Agents.Defaults.Workspace, dataDir, bundledSkillsDir, msgBus, toolsReg, providerRegistry, permPE.IsOwner, gatewayAddr, mcpToolLister)
if providersH != nil {
providersH.SetAPIBaseFallback(cfg.Providers.APIBaseForType)
}
@@ -327,7 +328,7 @@ func runGateway() {
server.SetTracesHandler(tracesH)
}
// External wake/trigger API
wakeH := httpapi.NewWakeHandler(agentRouter, cfg.Gateway.Token)
wakeH := httpapi.NewWakeHandler(agentRouter)
if postTurn != nil {
wakeH.SetPostTurnProcessor(postTurn)
}
@@ -351,7 +352,7 @@ func runGateway() {
server.SetTeamEventsHandler(teamEventsH)
}
if pgStores != nil && pgStores.Teams != nil {
server.SetTeamAttachmentsHandler(httpapi.NewTeamAttachmentsHandler(pgStores.Teams, cfg.Gateway.Token, workspace))
server.SetTeamAttachmentsHandler(httpapi.NewTeamAttachmentsHandler(pgStores.Teams, workspace))
}
if builtinToolsH != nil {
server.SetBuiltinToolsHandler(builtinToolsH)
@@ -371,23 +372,23 @@ func runGateway() {
// Activity audit log API
if pgStores.Activity != nil {
server.SetActivityHandler(httpapi.NewActivityHandler(pgStores.Activity, cfg.Gateway.Token))
server.SetActivityHandler(httpapi.NewActivityHandler(pgStores.Activity))
}
// Usage analytics API
if pgStores.Snapshots != nil {
server.SetUsageHandler(httpapi.NewUsageHandler(pgStores.Snapshots, pgStores.DB, cfg.Gateway.Token))
server.SetUsageHandler(httpapi.NewUsageHandler(pgStores.Snapshots, pgStores.DB))
}
// Runtime package management (install/uninstall system/pip/npm packages)
server.SetPackagesHandler(httpapi.NewPackagesHandler(cfg.Gateway.Token))
server.SetPackagesHandler(httpapi.NewPackagesHandler())
// API key management
// API documentation (OpenAPI spec + Swagger UI at /docs)
server.SetDocsHandler(httpapi.NewDocsHandler(cfg.Gateway.Token))
server.SetDocsHandler(httpapi.NewDocsHandler())
if pgStores != nil && pgStores.APIKeys != nil {
server.SetAPIKeysHandler(httpapi.NewAPIKeysHandler(pgStores.APIKeys, cfg.Gateway.Token, msgBus))
server.SetAPIKeysHandler(httpapi.NewAPIKeysHandler(pgStores.APIKeys, msgBus))
server.SetAPIKeyStore(pgStores.APIKeys)
httpapi.InitAPIKeyCache(pgStores.APIKeys, msgBus)
}
@@ -399,29 +400,29 @@ func runGateway() {
// Memory management API (wired directly, only needs MemoryStore + token)
if pgStores != nil && pgStores.Memory != nil {
server.SetMemoryHandler(httpapi.NewMemoryHandler(pgStores.Memory, cfg.Gateway.Token))
server.SetMemoryHandler(httpapi.NewMemoryHandler(pgStores.Memory))
}
// Knowledge graph API
if pgStores != nil && pgStores.KnowledgeGraph != nil {
server.SetKnowledgeGraphHandler(httpapi.NewKnowledgeGraphHandler(pgStores.KnowledgeGraph, providerRegistry, cfg.Gateway.Token))
server.SetKnowledgeGraphHandler(httpapi.NewKnowledgeGraphHandler(pgStores.KnowledgeGraph, providerRegistry))
}
// Workspace file serving endpoint — serves files by absolute path, auth-token protected.
// Supports media from any agent workspace (each agent has its own workspace from DB).
server.SetFilesHandler(httpapi.NewFilesHandler(cfg.Gateway.Token, workspace, dataDir))
server.SetFilesHandler(httpapi.NewFilesHandler(workspace, dataDir))
// Storage file management — browse/delete files under the resolved workspace directory.
// Uses GOCLAW_WORKSPACE (or default ~/.goclaw/workspace) so it works correctly
// in Docker deployments where volumes are mounted outside ~/.goclaw/.
server.SetStorageHandler(httpapi.NewStorageHandler(workspace, cfg.Gateway.Token))
server.SetStorageHandler(httpapi.NewStorageHandler(workspace))
// Media upload endpoint — accepts multipart file uploads, returns temp path + MIME type.
server.SetMediaUploadHandler(httpapi.NewMediaUploadHandler(cfg.Gateway.Token))
server.SetMediaUploadHandler(httpapi.NewMediaUploadHandler())
// Media serve endpoint — serves persisted media files by ID for WS/web clients.
if mediaStore != nil {
server.SetMediaServeHandler(httpapi.NewMediaServeHandler(mediaStore, cfg.Gateway.Token))
server.SetMediaServeHandler(httpapi.NewMediaServeHandler(mediaStore))
}
// Seed + apply builtin tool disables
@@ -907,7 +908,7 @@ func runGateway() {
// Tenant management RPC + HTTP
if pgStores.Tenants != nil {
methods.NewTenantsMethods(pgStores.Tenants, msgBus, workspace).Register(server.Router())
server.SetTenantsHandler(httpapi.NewTenantsHandler(pgStores.Tenants, cfg.Gateway.Token, msgBus, workspace))
server.SetTenantsHandler(httpapi.NewTenantsHandler(pgStores.Tenants, msgBus, workspace))
server.Router().SetTenantStore(pgStores.Tenants)
// Permission cache for tenant membership checks (tenant role, agent access, etc.)
permCache := cache.NewPermissionCache()
+12 -12
View File
@@ -10,7 +10,7 @@ import (
)
// wireHTTP creates HTTP handlers (agents + skills + traces + MCP + channel instances + providers + builtin tools + pending messages).
func wireHTTP(stores *store.Stores, token, defaultWorkspace, dataDir, bundledSkillsDir string, msgBus *bus.MessageBus, toolsReg *tools.Registry, providerReg *providers.Registry, isOwner func(string) bool, gatewayAddr string, mcpToolLister httpapi.MCPToolLister) (*httpapi.AgentsHandler, *httpapi.SkillsHandler, *httpapi.TracesHandler, *httpapi.MCPHandler, *httpapi.ChannelInstancesHandler, *httpapi.ProvidersHandler, *httpapi.BuiltinToolsHandler, *httpapi.PendingMessagesHandler, *httpapi.TeamEventsHandler, *httpapi.SecureCLIHandler, *httpapi.MCPUserCredentialsHandler) {
func wireHTTP(stores *store.Stores, defaultWorkspace, dataDir, bundledSkillsDir string, msgBus *bus.MessageBus, toolsReg *tools.Registry, providerReg *providers.Registry, isOwner func(string) bool, gatewayAddr string, mcpToolLister httpapi.MCPToolLister) (*httpapi.AgentsHandler, *httpapi.SkillsHandler, *httpapi.TracesHandler, *httpapi.MCPHandler, *httpapi.ChannelInstancesHandler, *httpapi.ProvidersHandler, *httpapi.BuiltinToolsHandler, *httpapi.PendingMessagesHandler, *httpapi.TeamEventsHandler, *httpapi.SecureCLIHandler, *httpapi.MCPUserCredentialsHandler) {
var agentsH *httpapi.AgentsHandler
var skillsH *httpapi.SkillsHandler
var tracesH *httpapi.TracesHandler
@@ -26,36 +26,36 @@ func wireHTTP(stores *store.Stores, token, defaultWorkspace, dataDir, bundledSki
if providerReg != nil {
summoner = httpapi.NewAgentSummoner(stores.Agents, providerReg, msgBus)
}
agentsH = httpapi.NewAgentsHandler(stores.Agents, token, defaultWorkspace, msgBus, summoner, isOwner)
agentsH = httpapi.NewAgentsHandler(stores.Agents, defaultWorkspace, msgBus, summoner, isOwner)
}
if stores != nil && stores.Skills != nil {
if pgSkills, ok := stores.Skills.(*pg.PGSkillStore); ok {
dirs := pgSkills.Dirs()
if len(dirs) > 0 {
skillsH = httpapi.NewSkillsHandler(pgSkills, dirs[0], dataDir, bundledSkillsDir, token, msgBus)
skillsH = httpapi.NewSkillsHandler(pgSkills, dirs[0], dataDir, bundledSkillsDir, msgBus)
}
}
}
if stores != nil && stores.Tracing != nil {
tracesH = httpapi.NewTracesHandler(stores.Tracing, token)
tracesH = httpapi.NewTracesHandler(stores.Tracing)
}
if stores != nil && stores.MCP != nil {
mcpH = httpapi.NewMCPHandler(stores.MCP, token, msgBus, mcpToolLister)
mcpH = httpapi.NewMCPHandler(stores.MCP, msgBus, mcpToolLister)
}
var mcpUserCredsH *httpapi.MCPUserCredentialsHandler
if stores != nil && stores.MCP != nil {
mcpUserCredsH = httpapi.NewMCPUserCredentialsHandler(stores.MCP, token)
mcpUserCredsH = httpapi.NewMCPUserCredentialsHandler(stores.MCP, stores.Tenants)
}
if stores != nil && stores.ChannelInstances != nil {
channelInstancesH = httpapi.NewChannelInstancesHandler(stores.ChannelInstances, stores.Agents, stores.ConfigPermissions, stores.Contacts, stores.Tenants, token, msgBus)
channelInstancesH = httpapi.NewChannelInstancesHandler(stores.ChannelInstances, stores.Agents, stores.ConfigPermissions, stores.Contacts, stores.Tenants, msgBus)
}
if stores != nil && stores.Providers != nil {
providersH = httpapi.NewProvidersHandler(stores.Providers, stores.ConfigSecrets, token, providerReg, gatewayAddr)
providersH = httpapi.NewProvidersHandler(stores.Providers, stores.ConfigSecrets, providerReg, gatewayAddr)
providersH.SetMessageBus(msgBus)
if stores.MCP != nil {
providersH.SetMCPServerLookup(buildMCPServerLookup(stores.MCP))
@@ -65,19 +65,19 @@ func wireHTTP(stores *store.Stores, token, defaultWorkspace, dataDir, bundledSki
var teamEventsH *httpapi.TeamEventsHandler
if stores != nil && stores.Teams != nil {
teamEventsH = httpapi.NewTeamEventsHandler(stores.Teams, token)
teamEventsH = httpapi.NewTeamEventsHandler(stores.Teams)
}
if stores != nil && stores.BuiltinTools != nil {
builtinToolsH = httpapi.NewBuiltinToolsHandler(stores.BuiltinTools, stores.BuiltinToolTenantCfgs, token, msgBus)
builtinToolsH = httpapi.NewBuiltinToolsHandler(stores.BuiltinTools, stores.BuiltinToolTenantCfgs, msgBus)
}
if stores != nil && stores.PendingMessages != nil {
pendingMessagesH = httpapi.NewPendingMessagesHandler(stores.PendingMessages, stores.Agents, token, providerReg)
pendingMessagesH = httpapi.NewPendingMessagesHandler(stores.PendingMessages, stores.Agents, providerReg)
}
if stores != nil && stores.SecureCLI != nil {
secureCLIH = httpapi.NewSecureCLIHandler(stores.SecureCLI, token, msgBus)
secureCLIH = httpapi.NewSecureCLIHandler(stores.SecureCLI, msgBus)
}
return agentsH, skillsH, tracesH, mcpH, channelInstancesH, providersH, builtinToolsH, pendingMessagesH, teamEventsH, secureCLIH, mcpUserCredsH
+6 -6
View File
@@ -162,7 +162,7 @@ func (s *Server) BuildMux() *http.ServeMux {
// OpenAI-compatible chat completions
isManaged := s.agentStore != nil
chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, s.cfg.Gateway.Token, isManaged)
chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, isManaged)
if s.rateLimiter.Enabled() {
chatHandler.SetRateLimiter(s.rateLimiter.Allow)
}
@@ -172,7 +172,7 @@ func (s *Server) BuildMux() *http.ServeMux {
mux.Handle("/v1/chat/completions", chatHandler)
// OpenResponses protocol
responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions, s.cfg.Gateway.Token)
responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions)
if s.postTurn != nil {
responsesHandler.SetPostTurnProcessor(s.postTurn)
}
@@ -180,7 +180,7 @@ func (s *Server) BuildMux() *http.ServeMux {
// Direct tool invocation
if s.tools != nil {
toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.cfg.Gateway.Token, s.agentStore)
toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.agentStore)
mux.Handle("/v1/tools/invoke", toolsHandler)
}
@@ -663,7 +663,7 @@ func StartTestServer(s *Server, ctx context.Context) (addr string, start func())
mux.HandleFunc("/health", s.handleHealth)
isManaged := s.agentStore != nil
chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, s.cfg.Gateway.Token, isManaged)
chatHandler := httpapi.NewChatCompletionsHandler(s.agents, s.sessions, isManaged)
if s.rateLimiter.Enabled() {
chatHandler.SetRateLimiter(s.rateLimiter.Allow)
}
@@ -672,14 +672,14 @@ func StartTestServer(s *Server, ctx context.Context) (addr string, start func())
}
mux.Handle("/v1/chat/completions", chatHandler)
responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions, s.cfg.Gateway.Token)
responsesHandler := httpapi.NewResponsesHandler(s.agents, s.sessions)
if s.postTurn != nil {
responsesHandler.SetPostTurnProcessor(s.postTurn)
}
mux.Handle("/v1/responses", responsesHandler)
if s.tools != nil {
toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.cfg.Gateway.Token, s.agentStore)
toolsHandler := httpapi.NewToolsInvokeHandler(s.tools, s.agentStore)
mux.Handle("/v1/tools/invoke", toolsHandler)
}
+4 -5
View File
@@ -11,12 +11,11 @@ import (
// ActivityHandler handles activity audit log endpoints.
type ActivityHandler struct {
activity store.ActivityStore
token string
}
// NewActivityHandler creates a handler for activity log endpoints.
func NewActivityHandler(activity store.ActivityStore, token string) *ActivityHandler {
return &ActivityHandler{activity: activity, token: token}
func NewActivityHandler(activity store.ActivityStore) *ActivityHandler {
return &ActivityHandler{activity: activity}
}
// RegisterRoutes registers activity routes on the given mux.
@@ -25,7 +24,7 @@ func (h *ActivityHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *ActivityHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
func (h *ActivityHandler) handleList(w http.ResponseWriter, r *http.Request) {
@@ -42,7 +41,7 @@ func (h *ActivityHandler) handleList(w http.ResponseWriter, r *http.Request) {
}
// Non-admin callers may only see their own activity logs.
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
callerID := store.UserIDFromContext(r.Context())
opts.ActorID = callerID
+3 -4
View File
@@ -20,7 +20,6 @@ import (
// AgentsHandler handles agent CRUD and sharing endpoints.
type AgentsHandler struct {
agents store.AgentStore
token string
defaultWorkspace string // default workspace path template (e.g. "~/.goclaw/workspace")
msgBus *bus.MessageBus // for cache invalidation events (nil = no events)
summoner *AgentSummoner // LLM-based agent setup (nil = disabled)
@@ -29,8 +28,8 @@ type AgentsHandler struct {
// NewAgentsHandler creates a handler for agent management endpoints.
// isOwner is a function that checks if a user ID is in GOCLAW_OWNER_IDS (nil = disabled).
func NewAgentsHandler(agents store.AgentStore, token, defaultWorkspace string, msgBus *bus.MessageBus, summoner *AgentSummoner, isOwner func(string) bool) *AgentsHandler {
return &AgentsHandler{agents: agents, token: token, defaultWorkspace: defaultWorkspace, msgBus: msgBus, summoner: summoner, isOwner: isOwner}
func NewAgentsHandler(agents store.AgentStore, defaultWorkspace string, msgBus *bus.MessageBus, summoner *AgentSummoner, isOwner func(string) bool) *AgentsHandler {
return &AgentsHandler{agents: agents, defaultWorkspace: defaultWorkspace, msgBus: msgBus, summoner: summoner, isOwner: isOwner}
}
// isOwnerUser checks if the given user ID is a system owner.
@@ -68,7 +67,7 @@ func (h *AgentsHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *AgentsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
func (h *AgentsHandler) handleList(w http.ResponseWriter, r *http.Request) {
+3 -4
View File
@@ -19,13 +19,12 @@ import (
// APIKeysHandler handles API key management endpoints.
type APIKeysHandler struct {
apiKeys store.APIKeyStore
token string // gateway token for admin auth
msgBus *bus.MessageBus // for cache invalidation events
}
// NewAPIKeysHandler creates a handler for API key management endpoints.
func NewAPIKeysHandler(apiKeys store.APIKeyStore, token string, msgBus *bus.MessageBus) *APIKeysHandler {
return &APIKeysHandler{apiKeys: apiKeys, token: token, msgBus: msgBus}
func NewAPIKeysHandler(apiKeys store.APIKeyStore, msgBus *bus.MessageBus) *APIKeysHandler {
return &APIKeysHandler{apiKeys: apiKeys, msgBus: msgBus}
}
// RegisterRoutes registers all API key management routes on the given mux.
@@ -37,7 +36,7 @@ func (h *APIKeysHandler) RegisterRoutes(mux *http.ServeMux) {
// adminAuth ensures the caller has admin access (gateway token or API key with admin scope).
func (h *APIKeysHandler) adminAuth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, permissions.RoleAdmin, next)
return requireAuth(permissions.RoleAdmin, next)
}
func (h *APIKeysHandler) handleList(w http.ResponseWriter, r *http.Request) {
+20 -11
View File
@@ -76,11 +76,18 @@ func extractAgentID(r *http.Request, model string) string {
// --- Package-level API key cache for shared auth ---
var pkgGatewayToken string
var pkgAPIKeyCache *apiKeyCache
var pkgPairingStore store.PairingStore
var pkgTenantCache *tenantCache
var pkgOwnerIDs []string
// InitGatewayToken sets the gateway bearer token for HTTP auth.
// Must be called once during server startup before handling requests.
func InitGatewayToken(token string) {
pkgGatewayToken = token
}
// InitAPIKeyCache initializes the shared API key cache with TTL and pubsub invalidation.
// Must be called once during server startup before handling requests.
func InitAPIKeyCache(s store.APIKeyStore, mb *bus.MessageBus) {
@@ -157,17 +164,17 @@ type authResult struct {
// resolveAuth determines the caller's role from the request.
// Priority: gateway token → API key → no-auth fallback.
func resolveAuth(r *http.Request, gatewayToken string) authResult {
return resolveAuthBearer(r, gatewayToken, extractBearerToken(r))
func resolveAuth(r *http.Request) authResult {
return resolveAuthWithBearer(r, extractBearerToken(r))
}
// resolveAuthBearer is like resolveAuth but accepts a pre-extracted bearer token.
// resolveAuthWithBearer is like resolveAuth but accepts a pre-extracted bearer token.
// Useful for handlers that also accept tokens from query params.
func resolveAuthBearer(r *http.Request, gatewayToken, bearer string) authResult {
func resolveAuthWithBearer(r *http.Request, bearer string) authResult {
// Gateway token → admin
// Only owner IDs get cross-tenant access; others are tenant-scoped.
// If no owner IDs configured, all gateway token users get cross-tenant (backward compat).
if gatewayToken != "" && tokenMatch(bearer, gatewayToken) {
if pkgGatewayToken != "" && tokenMatch(bearer, pkgGatewayToken) {
userID := extractUserID(r)
isOwner := isHTTPOwnerID(userID, pkgOwnerIDs)
res := authResult{Role: permissions.RoleAdmin, Authenticated: true, CrossTenant: isOwner}
@@ -221,7 +228,7 @@ func resolveAuthBearer(r *http.Request, gatewayToken, bearer string) authResult
}
}
// No auth configured → admin (no token = dev/single-user mode, full access)
if gatewayToken == "" {
if pkgGatewayToken == "" {
return authResult{Role: permissions.RoleAdmin, Authenticated: true, TenantID: store.MasterTenantID}
}
return authResult{}
@@ -239,11 +246,11 @@ func httpMinRole(method string) permissions.Role {
// requireAuth is a middleware that checks authentication and minimum role.
// Pass "" for minRole to auto-detect from HTTP method (GET→Viewer, POST→Operator).
// Injects locale and userID into request context.
func requireAuth(token string, minRole permissions.Role, next http.HandlerFunc) http.HandlerFunc {
// Injects locale, role, userID and tenantID into request context.
func requireAuth(minRole permissions.Role, next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
auth := resolveAuth(r, token)
auth := resolveAuth(r)
if !auth.Authenticated {
writeJSON(w, http.StatusUnauthorized, map[string]string{
@@ -265,6 +272,7 @@ func requireAuth(token string, minRole permissions.Role, next http.HandlerFunc)
}
ctx := store.WithLocale(r.Context(), locale)
ctx = store.WithRole(ctx, string(auth.Role))
userID := extractUserID(r)
// If the API key has a bound owner, force user_id to owner regardless of header.
if auth.KeyData != nil && auth.KeyData.OwnerID != "" {
@@ -318,9 +326,9 @@ func requireAuth(token string, minRole permissions.Role, next http.HandlerFunc)
// requireAuthBearer is like requireAuth but accepts a pre-extracted bearer token.
// Used by handlers that accept tokens from query params (files, media).
// Returns the authenticated request with user context applied (owner_id enforcement included).
func requireAuthBearer(token string, minRole permissions.Role, bearer string, w http.ResponseWriter, r *http.Request) (*http.Request, bool) {
func requireAuthBearer(minRole permissions.Role, bearer string, w http.ResponseWriter, r *http.Request) (*http.Request, bool) {
locale := extractLocale(r)
auth := resolveAuthBearer(r, token, bearer)
auth := resolveAuthWithBearer(r, bearer)
if !auth.Authenticated {
writeJSON(w, http.StatusUnauthorized, map[string]string{
@@ -343,6 +351,7 @@ func requireAuthBearer(token string, minRole permissions.Role, bearer string, w
// Apply user context + owner_id enforcement (same as requireAuth).
ctx := store.WithLocale(r.Context(), locale)
ctx = store.WithRole(ctx, string(auth.Role))
userID := extractUserID(r)
if auth.KeyData != nil && auth.KeyData.OwnerID != "" {
if userID != "" && userID != auth.KeyData.OwnerID {
+21 -8
View File
@@ -26,13 +26,22 @@ func setupTestCache(t *testing.T, keys map[string]*store.APIKeyData) *mockAPIKey
return ms
}
// setupTestToken sets the package-level gateway token for testing.
func setupTestToken(t *testing.T, token string) {
t.Helper()
old := pkgGatewayToken
pkgGatewayToken = token
t.Cleanup(func() { pkgGatewayToken = old })
}
func TestResolveAuth_GatewayToken(t *testing.T) {
setupTestCache(t, nil)
setupTestToken(t, "my-gateway-token")
r := httptest.NewRequest("GET", "/v1/agents", nil)
r.Header.Set("Authorization", "Bearer my-gateway-token")
auth := resolveAuth(r, "my-gateway-token")
auth := resolveAuth(r)
if !auth.Authenticated {
t.Fatal("expected authenticated")
}
@@ -43,11 +52,12 @@ func TestResolveAuth_GatewayToken(t *testing.T) {
func TestResolveAuth_WrongToken(t *testing.T) {
setupTestCache(t, nil)
setupTestToken(t, "correct-token")
r := httptest.NewRequest("GET", "/v1/agents", nil)
r.Header.Set("Authorization", "Bearer wrong-token")
auth := resolveAuth(r, "correct-token")
auth := resolveAuth(r)
if auth.Authenticated {
t.Fatal("expected unauthenticated for wrong token")
}
@@ -58,7 +68,7 @@ func TestResolveAuth_NoAuthConfigured(t *testing.T) {
r := httptest.NewRequest("GET", "/v1/agents", nil)
auth := resolveAuth(r, "") // no gateway token configured
auth := resolveAuth(r) // no gateway token configured
if !auth.Authenticated {
t.Fatal("expected authenticated when no token configured")
}
@@ -155,8 +165,9 @@ func TestHttpMinRole(t *testing.T) {
func TestRequireAuth_Unauthorized(t *testing.T) {
setupTestCache(t, nil)
setupTestToken(t, "secret")
handler := requireAuth("secret", "", func(w http.ResponseWriter, r *http.Request) {
handler := requireAuth("", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
@@ -171,8 +182,9 @@ func TestRequireAuth_Unauthorized(t *testing.T) {
func TestRequireAuth_GatewayTokenPasses(t *testing.T) {
setupTestCache(t, nil)
setupTestToken(t, "secret")
handler := requireAuth("secret", "", func(w http.ResponseWriter, r *http.Request) {
handler := requireAuth("", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
@@ -188,9 +200,10 @@ func TestRequireAuth_GatewayTokenPasses(t *testing.T) {
func TestRequireAuth_InjectLocaleAndUserID(t *testing.T) {
setupTestCache(t, nil)
setupTestToken(t, "secret")
var gotLocale, gotUserID string
handler := requireAuth("secret", "", func(w http.ResponseWriter, r *http.Request) {
handler := requireAuth("", func(w http.ResponseWriter, r *http.Request) {
gotLocale = store.LocaleFromContext(r.Context())
gotUserID = store.UserIDFromContext(r.Context())
w.WriteHeader(http.StatusOK)
@@ -218,7 +231,7 @@ func TestRequireAuth_AdminRoleEnforced(t *testing.T) {
// No auth configured → admin role (dev/single-user mode) → admin endpoint accessible
setupTestCache(t, nil)
handler := requireAuth("", permissions.RoleAdmin, func(w http.ResponseWriter, r *http.Request) {
handler := requireAuth(permissions.RoleAdmin, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
@@ -236,7 +249,7 @@ func TestRequireAuth_AutoDetectRole_GET(t *testing.T) {
// No auth configured → operator role. GET needs viewer → passes.
setupTestCache(t, nil)
handler := requireAuth("", "", func(w http.ResponseWriter, r *http.Request) {
handler := requireAuth("", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
+3 -4
View File
@@ -18,13 +18,12 @@ import (
type BuiltinToolsHandler struct {
store store.BuiltinToolStore
tenantCfgStore store.BuiltinToolTenantConfigStore
token string
msgBus *bus.MessageBus
}
// NewBuiltinToolsHandler creates a handler for built-in tool management endpoints.
func NewBuiltinToolsHandler(s store.BuiltinToolStore, tenantCfgs store.BuiltinToolTenantConfigStore, token string, msgBus *bus.MessageBus) *BuiltinToolsHandler {
return &BuiltinToolsHandler{store: s, tenantCfgStore: tenantCfgs, token: token, msgBus: msgBus}
func NewBuiltinToolsHandler(s store.BuiltinToolStore, tenantCfgs store.BuiltinToolTenantConfigStore, msgBus *bus.MessageBus) *BuiltinToolsHandler {
return &BuiltinToolsHandler{store: s, tenantCfgStore: tenantCfgs, msgBus: msgBus}
}
// RegisterRoutes registers all built-in tool routes on the given mux.
@@ -37,7 +36,7 @@ func (h *BuiltinToolsHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *BuiltinToolsHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
func (h *BuiltinToolsHandler) emitCacheInvalidate(key string) {
+3 -4
View File
@@ -22,13 +22,12 @@ type ChannelInstancesHandler struct {
configPermStore store.ConfigPermissionStore
contactStore store.ContactStore
tenantStore store.TenantStore
token string
msgBus *bus.MessageBus
}
// NewChannelInstancesHandler creates a handler for channel instance management endpoints.
func NewChannelInstancesHandler(s store.ChannelInstanceStore, agentStore store.AgentStore, configPermStore store.ConfigPermissionStore, contactStore store.ContactStore, tenantStore store.TenantStore, token string, msgBus *bus.MessageBus) *ChannelInstancesHandler {
return &ChannelInstancesHandler{store: s, agentStore: agentStore, configPermStore: configPermStore, contactStore: contactStore, tenantStore: tenantStore, token: token, msgBus: msgBus}
func NewChannelInstancesHandler(s store.ChannelInstanceStore, agentStore store.AgentStore, configPermStore store.ConfigPermissionStore, contactStore store.ContactStore, tenantStore store.TenantStore, msgBus *bus.MessageBus) *ChannelInstancesHandler {
return &ChannelInstancesHandler{store: s, agentStore: agentStore, configPermStore: configPermStore, contactStore: contactStore, tenantStore: tenantStore, msgBus: msgBus}
}
// RegisterRoutes registers all channel instance routes on the given mux.
@@ -61,7 +60,7 @@ func (h *ChannelInstancesHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *ChannelInstancesHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
func (h *ChannelInstancesHandler) emitCacheInvalidate() {
+2 -4
View File
@@ -21,7 +21,6 @@ import (
type ChatCompletionsHandler struct {
agents *agent.Router
sessions store.SessionStore
token string // expected bearer token (empty = no auth)
isManaged bool
rateLimiter func(string) bool // rate limit check: key → allowed (nil = no limit)
postTurn tools.PostTurnProcessor
@@ -33,11 +32,10 @@ func (h *ChatCompletionsHandler) SetPostTurnProcessor(pt tools.PostTurnProcessor
}
// NewChatCompletionsHandler creates a handler for the chat completions endpoint.
func NewChatCompletionsHandler(agents *agent.Router, sess store.SessionStore, token string, isManaged bool) *ChatCompletionsHandler {
func NewChatCompletionsHandler(agents *agent.Router, sess store.SessionStore, isManaged bool) *ChatCompletionsHandler {
return &ChatCompletionsHandler{
agents: agents,
sessions: sess,
token: token,
isManaged: isManaged,
}
}
@@ -91,7 +89,7 @@ func (h *ChatCompletionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
}
// Auth + RBAC check (gateway token or API key, operator required for POST)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !auth.Authenticated {
http.Error(w, fmt.Sprintf(`{"error":{"message":"%s","type":"invalid_request_error"}}`, i18n.T(locale, i18n.MsgInvalidAuth)), http.StatusUnauthorized)
return
+3 -4
View File
@@ -18,7 +18,6 @@ import (
// When an exact path is not found, falls back to searching the workspace for
// generated files by basename (goclaw_gen_* filenames are globally unique).
type FilesHandler struct {
token string
workspace string // workspace root for fallback file search
dataDir string // data directory root for tenant path validation
}
@@ -26,8 +25,8 @@ type FilesHandler struct {
// NewFilesHandler creates a handler that serves files by absolute path.
// workspace is the root directory used for fallback generated file search.
// dataDir is used for tenant path validation (files must be within tenant's dirs).
func NewFilesHandler(token, workspace, dataDir string) *FilesHandler {
return &FilesHandler{token: token, workspace: workspace, dataDir: dataDir}
func NewFilesHandler(workspace, dataDir string) *FilesHandler {
return &FilesHandler{workspace: workspace, dataDir: dataDir}
}
// RegisterRoutes registers the file serving route.
@@ -49,7 +48,7 @@ func (h *FilesHandler) auth(next http.HandlerFunc) http.HandlerFunc {
}
// Priority 2: Bearer header (API clients only).
provided := extractBearerToken(r)
authedReq, ok := requireAuthBearer(h.token, "", provided, w, r)
authedReq, ok := requireAuthBearer("", provided, w, r)
if !ok {
return
}
+3 -4
View File
@@ -13,12 +13,11 @@ import (
type KnowledgeGraphHandler struct {
store store.KnowledgeGraphStore
providerReg *providers.Registry
token string
}
// NewKnowledgeGraphHandler creates a handler for KG management endpoints.
func NewKnowledgeGraphHandler(s store.KnowledgeGraphStore, providerReg *providers.Registry, token string) *KnowledgeGraphHandler {
return &KnowledgeGraphHandler{store: s, providerReg: providerReg, token: token}
func NewKnowledgeGraphHandler(s store.KnowledgeGraphStore, providerReg *providers.Registry) *KnowledgeGraphHandler {
return &KnowledgeGraphHandler{store: s, providerReg: providerReg}
}
// NewExtractor creates an Extractor from the given provider name and model.
@@ -46,5 +45,5 @@ func (h *KnowledgeGraphHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *KnowledgeGraphHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
+5 -6
View File
@@ -26,15 +26,14 @@ type MCPPoolEvictor interface {
// MCPHandler handles MCP server management HTTP endpoints.
type MCPHandler struct {
store store.MCPServerStore
token string
msgBus *bus.MessageBus
mgr MCPToolLister // optional, nil when Manager not available
poolEvictor MCPPoolEvictor // optional, nil when pool not available
mgr MCPToolLister // optional, nil when Manager not available
poolEvictor MCPPoolEvictor // optional, nil when pool not available
}
// NewMCPHandler creates a handler for MCP server management endpoints.
func NewMCPHandler(s store.MCPServerStore, token string, msgBus *bus.MessageBus, mgr MCPToolLister) *MCPHandler {
return &MCPHandler{store: s, token: token, msgBus: msgBus, mgr: mgr}
func NewMCPHandler(s store.MCPServerStore, msgBus *bus.MessageBus, mgr MCPToolLister) *MCPHandler {
return &MCPHandler{store: s, msgBus: msgBus, mgr: mgr}
}
// SetPoolEvictor sets the pool evictor for credential rotation handling.
@@ -82,7 +81,7 @@ func (h *MCPHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *MCPHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
// --- Server CRUD ---
+3 -4
View File
@@ -14,12 +14,11 @@ import (
// MediaServeHandler serves persisted media files by ID.
type MediaServeHandler struct {
store *media.Store
token string
}
// NewMediaServeHandler creates a media serve handler.
func NewMediaServeHandler(store *media.Store, token string) *MediaServeHandler {
return &MediaServeHandler{store: store, token: token}
func NewMediaServeHandler(store *media.Store) *MediaServeHandler {
return &MediaServeHandler{store: store}
}
// RegisterRoutes registers the media serve endpoint.
@@ -41,7 +40,7 @@ func (h *MediaServeHandler) auth(next http.HandlerFunc) http.HandlerFunc {
}
// Priority 2: Bearer header (API clients only).
provided := extractBearerToken(r)
authedReq, ok := requireAuthBearer(h.token, "", provided, w, r)
authedReq, ok := requireAuthBearer("", provided, w, r)
if !ok {
return
}
+4 -6
View File
@@ -19,13 +19,11 @@ const (
)
// MediaUploadHandler handles media file uploads for WebSocket clients.
type MediaUploadHandler struct {
token string
}
type MediaUploadHandler struct{}
// NewMediaUploadHandler creates a media upload handler.
func NewMediaUploadHandler(token string) *MediaUploadHandler {
return &MediaUploadHandler{token: token}
func NewMediaUploadHandler() *MediaUploadHandler {
return &MediaUploadHandler{}
}
// RegisterRoutes registers the upload endpoint.
@@ -34,7 +32,7 @@ func (h *MediaUploadHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *MediaUploadHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
func (h *MediaUploadHandler) handleUpload(w http.ResponseWriter, r *http.Request) {
+3 -4
View File
@@ -9,12 +9,11 @@ import (
// MemoryHandler handles memory document management endpoints.
type MemoryHandler struct {
store store.MemoryStore
token string
}
// NewMemoryHandler creates a handler for memory management endpoints.
func NewMemoryHandler(s store.MemoryStore, token string) *MemoryHandler {
return &MemoryHandler{store: s, token: token}
func NewMemoryHandler(s store.MemoryStore) *MemoryHandler {
return &MemoryHandler{store: s}
}
// RegisterRoutes registers all memory routes on the given mux.
@@ -31,5 +30,5 @@ func (h *MemoryHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *MemoryHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
+2 -4
View File
@@ -19,7 +19,6 @@ import (
// OAuthHandler handles OAuth-related HTTP endpoints for web UI.
type OAuthHandler struct {
token string // gateway auth token
provStore store.ProviderStore
secretStore store.ConfigSecretsStore
providerReg *providers.Registry
@@ -30,9 +29,8 @@ type OAuthHandler struct {
}
// NewOAuthHandler creates a handler for OAuth endpoints.
func NewOAuthHandler(token string, provStore store.ProviderStore, secretStore store.ConfigSecretsStore, providerReg *providers.Registry, msgBus *bus.MessageBus) *OAuthHandler {
func NewOAuthHandler(provStore store.ProviderStore, secretStore store.ConfigSecretsStore, providerReg *providers.Registry, msgBus *bus.MessageBus) *OAuthHandler {
return &OAuthHandler{
token: token,
provStore: provStore,
secretStore: secretStore,
providerReg: providerReg,
@@ -49,7 +47,7 @@ func (h *OAuthHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *OAuthHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
func (h *OAuthHandler) newTokenSource(r *http.Request) *oauth.DBTokenSource {
+11 -7
View File
@@ -114,14 +114,18 @@ func (m *mockSecretsStore) GetAll(_ context.Context) (map[string]string, error)
// --- helper ---
func newTestOAuthHandler(token string) *OAuthHandler {
return NewOAuthHandler(token, newMockProviderStore(), newMockSecretsStore(), nil, nil)
func newTestOAuthHandler(t *testing.T, token string) *OAuthHandler {
t.Helper()
old := pkgGatewayToken
pkgGatewayToken = token
t.Cleanup(func() { pkgGatewayToken = old })
return NewOAuthHandler(newMockProviderStore(), newMockSecretsStore(), nil, nil)
}
// --- tests ---
func TestOAuthHandlerStatusNoToken(t *testing.T) {
h := newTestOAuthHandler("")
h := newTestOAuthHandler(t, "")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
@@ -142,7 +146,7 @@ func TestOAuthHandlerStatusNoToken(t *testing.T) {
}
func TestOAuthHandlerAuth(t *testing.T) {
h := newTestOAuthHandler("secret-token")
h := newTestOAuthHandler(t, "secret-token")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
@@ -167,7 +171,7 @@ func TestOAuthHandlerAuth(t *testing.T) {
}
func TestOAuthHandlerLogoutNoProvider(t *testing.T) {
h := newTestOAuthHandler("")
h := newTestOAuthHandler(t, "")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
@@ -188,7 +192,7 @@ func TestOAuthHandlerLogoutNoProvider(t *testing.T) {
}
func TestOAuthHandlerRouteRegistration(t *testing.T) {
h := newTestOAuthHandler("")
h := newTestOAuthHandler(t, "")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
@@ -212,7 +216,7 @@ func TestOAuthHandlerRouteRegistration(t *testing.T) {
}
func TestOAuthHandlerStartReturnsAuthURL(t *testing.T) {
h := newTestOAuthHandler("")
h := newTestOAuthHandler(t, "")
mux := http.NewServeMux()
h.RegisterRoutes(mux)
+3 -5
View File
@@ -9,13 +9,11 @@ import (
var openapiSpec []byte
// DocsHandler serves the OpenAPI spec and Swagger UI.
type DocsHandler struct {
token string
}
type DocsHandler struct{}
// NewDocsHandler creates a handler for API documentation endpoints.
func NewDocsHandler(token string) *DocsHandler {
return &DocsHandler{token: token}
func NewDocsHandler() *DocsHandler {
return &DocsHandler{}
}
// RegisterRoutes registers documentation routes on the given mux.
+5 -7
View File
@@ -15,13 +15,11 @@ import (
var validPkgName = regexp.MustCompile(`^[a-zA-Z0-9@][a-zA-Z0-9._+\-/@]*$`)
// PackagesHandler handles runtime package management HTTP endpoints.
type PackagesHandler struct {
token string
}
type PackagesHandler struct{}
// NewPackagesHandler creates a handler for package management endpoints.
func NewPackagesHandler(token string) *PackagesHandler {
return &PackagesHandler{token: token}
func NewPackagesHandler() *PackagesHandler {
return &PackagesHandler{}
}
// RegisterRoutes registers all package management routes on the given mux.
@@ -35,14 +33,14 @@ func (h *PackagesHandler) RegisterRoutes(mux *http.ServeMux) {
// readAuth allows viewer+ for read operations.
func (h *PackagesHandler) readAuth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
// adminAuth requires admin role for write operations (install/uninstall).
// Prevents agents from calling these endpoints even if they obtain the gateway token,
// since agent requests via browser pairing only get operator role.
func (h *PackagesHandler) adminAuth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, permissions.RoleAdmin, next)
return requireAuth(permissions.RoleAdmin, next)
}
// handleList returns all installed packages grouped by category (system/pip/npm).
+2 -5
View File
@@ -163,18 +163,15 @@ func TestParseAndValidatePackage_BodySizeLimit(t *testing.T) {
// TestNewPackagesHandler creates a handler.
func TestNewPackagesHandler(t *testing.T) {
h := NewPackagesHandler("test-token")
h := NewPackagesHandler()
if h == nil {
t.Fatal("NewPackagesHandler() returned nil")
}
if h.token != "test-token" {
t.Errorf("token = %q, want 'test-token'", h.token)
}
}
// TestPackagesHandler_RegisterRoutes ensures routes are registered without panic.
func TestPackagesHandler_RegisterRoutes(t *testing.T) {
h := NewPackagesHandler("test-token")
h := NewPackagesHandler()
mux := http.NewServeMux()
// Should not panic
+3 -4
View File
@@ -17,7 +17,6 @@ import (
type PendingMessagesHandler struct {
store store.PendingMessageStore
agentStore store.AgentStore
token string
providerReg *providers.Registry
keepRecent int // global keepRecent from config (0 = use default 15)
maxTokens int // max output tokens for LLM summarization (0 = use default)
@@ -25,8 +24,8 @@ type PendingMessagesHandler struct {
cfgModel string // config-level model override (empty = resolve from agent)
}
func NewPendingMessagesHandler(s store.PendingMessageStore, agentStore store.AgentStore, token string, providerReg *providers.Registry) *PendingMessagesHandler {
return &PendingMessagesHandler{store: s, agentStore: agentStore, token: token, providerReg: providerReg}
func NewPendingMessagesHandler(s store.PendingMessageStore, agentStore store.AgentStore, providerReg *providers.Registry) *PendingMessagesHandler {
return &PendingMessagesHandler{store: s, agentStore: agentStore, providerReg: providerReg}
}
// SetKeepRecent sets the global keepRecent value from config.
@@ -49,7 +48,7 @@ func (h *PendingMessagesHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *PendingMessagesHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
// GET /v1/pending-messages — list all groups with resolved titles
+4 -5
View File
@@ -20,7 +20,6 @@ import (
type ProvidersHandler struct {
store store.ProviderStore
secretStore store.ConfigSecretsStore
token string
providerReg *providers.Registry
gatewayAddr string // for injecting MCP bridge into Claude CLI providers
mcpLookup providers.MCPServerLookup // optional: resolves per-agent MCP servers
@@ -30,8 +29,8 @@ type ProvidersHandler struct {
}
// NewProvidersHandler creates a handler for provider management endpoints.
func NewProvidersHandler(s store.ProviderStore, secretStore store.ConfigSecretsStore, token string, providerReg *providers.Registry, gatewayAddr string) *ProvidersHandler {
return &ProvidersHandler{store: s, secretStore: secretStore, token: token, providerReg: providerReg, gatewayAddr: gatewayAddr}
func NewProvidersHandler(s store.ProviderStore, secretStore store.ConfigSecretsStore, providerReg *providers.Registry, gatewayAddr string) *ProvidersHandler {
return &ProvidersHandler{store: s, secretStore: secretStore, providerReg: providerReg, gatewayAddr: gatewayAddr}
}
// SetMessageBus sets the message bus for audit event broadcasting.
@@ -95,7 +94,7 @@ func (h *ProvidersHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *ProvidersHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
// maskAPIKey replaces non-empty API keys with "***".
@@ -125,7 +124,7 @@ func (h *ProvidersHandler) registerInMemory(p *store.LLMProviderData) {
var cliOpts []providers.ClaudeCLIOption
cliOpts = append(cliOpts, providers.WithClaudeCLISecurityHooks("", true))
if h.gatewayAddr != "" {
mcpData := providers.BuildCLIMCPConfigData(nil, h.gatewayAddr, h.token)
mcpData := providers.BuildCLIMCPConfigData(nil, h.gatewayAddr, pkgGatewayToken)
mcpData.AgentMCPLookup = h.mcpLookup
cliOpts = append(cliOpts, providers.WithClaudeCLIMCPConfigData(mcpData))
}
+2 -4
View File
@@ -20,7 +20,6 @@ import (
type ResponsesHandler struct {
agents *agent.Router
sessions store.SessionStore
token string
postTurn tools.PostTurnProcessor
}
@@ -30,11 +29,10 @@ func (h *ResponsesHandler) SetPostTurnProcessor(pt tools.PostTurnProcessor) {
}
// NewResponsesHandler creates a handler for the responses endpoint.
func NewResponsesHandler(agents *agent.Router, sess store.SessionStore, token string) *ResponsesHandler {
func NewResponsesHandler(agents *agent.Router, sess store.SessionStore) *ResponsesHandler {
return &ResponsesHandler{
agents: agents,
sessions: sess,
token: token,
}
}
@@ -52,7 +50,7 @@ func (h *ResponsesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// Auth + RBAC check (gateway token or API key, operator required for POST)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !auth.Authenticated {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
+3 -4
View File
@@ -20,13 +20,12 @@ import (
// SecureCLIHandler handles secure CLI binary credential CRUD endpoints.
type SecureCLIHandler struct {
store store.SecureCLIStore
token string
msgBus *bus.MessageBus
}
// NewSecureCLIHandler creates a handler for secure CLI credential management.
func NewSecureCLIHandler(s store.SecureCLIStore, token string, msgBus *bus.MessageBus) *SecureCLIHandler {
return &SecureCLIHandler{store: s, token: token, msgBus: msgBus}
func NewSecureCLIHandler(s store.SecureCLIStore, msgBus *bus.MessageBus) *SecureCLIHandler {
return &SecureCLIHandler{store: s, msgBus: msgBus}
}
// RegisterRoutes registers all secure CLI routes on the given mux.
@@ -41,7 +40,7 @@ func (h *SecureCLIHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *SecureCLIHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, permissions.RoleAdmin, next)
return requireAuth(permissions.RoleAdmin, next)
}
func (h *SecureCLIHandler) emitCacheInvalidate(key string) {
+6 -7
View File
@@ -28,13 +28,12 @@ type SkillsHandler struct {
baseDir string // filesystem base for skill content (skills-store/) — master tenant
dataDir string // parent data dir for tenant-scoped skill paths
bundledDir string // original bundled skills dir (fallback for broken managed copies)
token string
msgBus *bus.MessageBus
}
// NewSkillsHandler creates a handler for skill management endpoints.
func NewSkillsHandler(skills *pg.PGSkillStore, baseDir, dataDir, bundledDir, token string, msgBus *bus.MessageBus) *SkillsHandler {
return &SkillsHandler{skills: skills, baseDir: baseDir, dataDir: dataDir, bundledDir: bundledDir, token: token, msgBus: msgBus}
func NewSkillsHandler(skills *pg.PGSkillStore, baseDir, dataDir, bundledDir string, msgBus *bus.MessageBus) *SkillsHandler {
return &SkillsHandler{skills: skills, baseDir: baseDir, dataDir: dataDir, bundledDir: bundledDir, msgBus: msgBus}
}
// tenantSkillsDir returns the skills-store directory scoped to the requesting tenant.
@@ -81,13 +80,13 @@ func (h *SkillsHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *SkillsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
// adminMiddleware requires admin role — used for system-level operations
// (rescan deps, install packages, toggle skills) that affect the entire server.
func (h *SkillsHandler) adminMiddleware(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, permissions.RoleAdmin, next)
return requireAuth(permissions.RoleAdmin, next)
}
// requireMasterTenant rejects requests from non-master tenants.
@@ -135,7 +134,7 @@ func (h *SkillsHandler) handleUpdate(w http.ResponseWriter, r *http.Request) {
}
// Ownership check (admins bypass)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
userID := store.UserIDFromContext(r.Context())
if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), id); found && ownerID != userID {
@@ -176,7 +175,7 @@ func (h *SkillsHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
}
// Ownership check (admins bypass)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
userID := store.UserIDFromContext(r.Context())
if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), id); found && ownerID != userID {
+4 -4
View File
@@ -45,7 +45,7 @@ func (h *SkillsHandler) handleGrantAgent(w http.ResponseWriter, r *http.Request)
}
// Ownership check (admins bypass)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), skillID); found && ownerID != userID {
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only the skill owner can perform this action"})
@@ -93,7 +93,7 @@ func (h *SkillsHandler) handleRevokeAgent(w http.ResponseWriter, r *http.Request
}
// Ownership check (admins bypass)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
userID := store.UserIDFromContext(r.Context())
if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), skillID); found && ownerID != userID {
@@ -131,7 +131,7 @@ func (h *SkillsHandler) handleGrantUser(w http.ResponseWriter, r *http.Request)
}
// Ownership check (admins bypass)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), skillID); found && ownerID != userID {
writeJSON(w, http.StatusForbidden, map[string]string{"error": "only the skill owner can perform this action"})
@@ -176,7 +176,7 @@ func (h *SkillsHandler) handleRevokeUser(w http.ResponseWriter, r *http.Request)
}
// Ownership check (admins bypass)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
userID := store.UserIDFromContext(r.Context())
if ownerID, found := h.skills.GetSkillOwnerID(r.Context(), skillID); found && ownerID != userID {
+3 -4
View File
@@ -31,15 +31,14 @@ type sizeCacheEntry struct {
type StorageHandler struct {
baseDir string // global data dir (resolved absolute path to ~/.goclaw/)
token string
// sizeCache caches the total storage size per tenant for 60 minutes.
sizeCache sync.Map // tenantBaseDir (string) → *sizeCacheEntry
}
// NewStorageHandler creates a handler for workspace storage management.
func NewStorageHandler(baseDir, token string) *StorageHandler {
return &StorageHandler{baseDir: baseDir, token: token}
func NewStorageHandler(baseDir string) *StorageHandler {
return &StorageHandler{baseDir: baseDir}
}
// RegisterRoutes registers storage management routes on the given mux.
@@ -51,7 +50,7 @@ func (h *StorageHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *StorageHandler) auth(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
// tenantBaseDir resolves the data directory scoped to the requesting tenant.
+3 -4
View File
@@ -14,13 +14,12 @@ import (
// TeamAttachmentsHandler serves team task attachment files for download.
type TeamAttachmentsHandler struct {
teamStore store.TeamStore
token string
dataDir string
}
// NewTeamAttachmentsHandler creates a new handler for serving team attachments.
func NewTeamAttachmentsHandler(teamStore store.TeamStore, token, dataDir string) *TeamAttachmentsHandler {
return &TeamAttachmentsHandler{teamStore: teamStore, token: token, dataDir: dataDir}
func NewTeamAttachmentsHandler(teamStore store.TeamStore, dataDir string) *TeamAttachmentsHandler {
return &TeamAttachmentsHandler{teamStore: teamStore, dataDir: dataDir}
}
// RegisterRoutes registers the attachment download endpoint.
@@ -46,7 +45,7 @@ func (h *TeamAttachmentsHandler) authMiddleware(next http.HandlerFunc) http.Hand
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
authedReq, ok := requireAuthBearer(h.token, "", provided, w, r)
authedReq, ok := requireAuthBearer("", provided, w, r)
if !ok {
return
}
+3 -14
View File
@@ -13,11 +13,10 @@ import (
// TeamEventsHandler handles team event history HTTP endpoints.
type TeamEventsHandler struct {
teamStore store.TeamStore
token string
}
func NewTeamEventsHandler(teamStore store.TeamStore, token string) *TeamEventsHandler {
return &TeamEventsHandler{teamStore: teamStore, token: token}
func NewTeamEventsHandler(teamStore store.TeamStore) *TeamEventsHandler {
return &TeamEventsHandler{teamStore: teamStore}
}
func (h *TeamEventsHandler) RegisterRoutes(mux *http.ServeMux) {
@@ -25,17 +24,7 @@ func (h *TeamEventsHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *TeamEventsHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if h.token != "" {
provided := extractBearerToken(r)
if !tokenMatch(provided, h.token) {
locale := extractLocale(r)
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
return
}
}
next(w, r)
}
return requireAuth("", next)
}
func (h *TeamEventsHandler) handleList(w http.ResponseWriter, r *http.Request) {
+3 -4
View File
@@ -19,20 +19,19 @@ import (
// TenantsHandler handles tenant CRUD and membership endpoints.
type TenantsHandler struct {
tenantStore store.TenantStore
token string
msgBus *bus.MessageBus
workspace string // base workspace directory for tenant dirs
}
// NewTenantsHandler creates a handler for tenant management endpoints.
func NewTenantsHandler(tenantStore store.TenantStore, token string, msgBus *bus.MessageBus, workspace string) *TenantsHandler {
return &TenantsHandler{tenantStore: tenantStore, token: token, msgBus: msgBus, workspace: workspace}
func NewTenantsHandler(tenantStore store.TenantStore, msgBus *bus.MessageBus, workspace string) *TenantsHandler {
return &TenantsHandler{tenantStore: tenantStore, msgBus: msgBus, workspace: workspace}
}
// RegisterRoutes registers all tenant management routes on the given mux.
func (h *TenantsHandler) RegisterRoutes(mux *http.ServeMux) {
admin := func(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, permissions.RoleAdmin, next)
return requireAuth(permissions.RoleAdmin, next)
}
mux.HandleFunc("GET /v1/tenants", admin(h.handleList))
mux.HandleFunc("POST /v1/tenants", admin(h.handleCreate))
+2 -4
View File
@@ -14,15 +14,13 @@ import (
// ToolsInvokeHandler handles POST /v1/tools/invoke (direct tool invocation).
type ToolsInvokeHandler struct {
registry *tools.Registry
token string
agentStore store.AgentStore // nil if not configured
}
// NewToolsInvokeHandler creates a handler for the tools invoke endpoint.
func NewToolsInvokeHandler(registry *tools.Registry, token string, agentStore store.AgentStore) *ToolsInvokeHandler {
func NewToolsInvokeHandler(registry *tools.Registry, agentStore store.AgentStore) *ToolsInvokeHandler {
return &ToolsInvokeHandler{
registry: registry,
token: token,
agentStore: agentStore,
}
}
@@ -47,7 +45,7 @@ func (h *ToolsInvokeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !auth.Authenticated {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
return
+6 -7
View File
@@ -19,12 +19,11 @@ import (
// TracesHandler handles LLM trace listing and detail endpoints.
type TracesHandler struct {
tracing store.TracingStore
token string
}
// NewTracesHandler creates a handler for trace management endpoints.
func NewTracesHandler(tracing store.TracingStore, token string) *TracesHandler {
return &TracesHandler{tracing: tracing, token: token}
func NewTracesHandler(tracing store.TracingStore) *TracesHandler {
return &TracesHandler{tracing: tracing}
}
// RegisterRoutes registers trace routes on the given mux.
@@ -36,7 +35,7 @@ func (h *TracesHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *TracesHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
func (h *TracesHandler) handleList(w http.ResponseWriter, r *http.Request) {
@@ -75,7 +74,7 @@ func (h *TracesHandler) handleList(w http.ResponseWriter, r *http.Request) {
}
// Non-admin callers may only see their own traces.
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
callerID := store.UserIDFromContext(r.Context())
opts.UserID = callerID
@@ -113,7 +112,7 @@ func (h *TracesHandler) handleGet(w http.ResponseWriter, r *http.Request) {
}
// Non-admin callers may only access their own traces.
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !permissions.HasMinRole(auth.Role, permissions.RoleAdmin) {
callerID := store.UserIDFromContext(r.Context())
if trace.UserID != callerID {
@@ -184,7 +183,7 @@ func (h *TracesHandler) handleExport(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": i18n.T(locale, i18n.MsgNotFound, "trace", traceID.String())})
return
}
authExport := resolveAuth(r, h.token)
authExport := resolveAuth(r)
if !permissions.HasMinRole(authExport.Role, permissions.RoleAdmin) {
callerID := store.UserIDFromContext(r.Context())
if rootTrace.UserID != callerID {
+3 -4
View File
@@ -16,11 +16,10 @@ import (
type UsageHandler struct {
snapshots store.SnapshotStore
db *sql.DB
token string
}
func NewUsageHandler(snapshots store.SnapshotStore, db *sql.DB, token string) *UsageHandler {
return &UsageHandler{snapshots: snapshots, db: db, token: token}
func NewUsageHandler(snapshots store.SnapshotStore, db *sql.DB) *UsageHandler {
return &UsageHandler{snapshots: snapshots, db: db}
}
func (h *UsageHandler) RegisterRoutes(mux *http.ServeMux) {
@@ -30,7 +29,7 @@ func (h *UsageHandler) RegisterRoutes(mux *http.ServeMux) {
}
func (h *UsageHandler) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return requireAuth(h.token, "", next)
return requireAuth("", next)
}
func (h *UsageHandler) handleTimeSeries(w http.ResponseWriter, r *http.Request) {
+3 -4
View File
@@ -20,7 +20,6 @@ import (
// Allows orchestrators (Paperclip, n8n, etc.) to trigger agent runs via HTTP.
type WakeHandler struct {
agents *agent.Router
token string
postTurn tools.PostTurnProcessor
}
@@ -30,8 +29,8 @@ func (h *WakeHandler) SetPostTurnProcessor(pt tools.PostTurnProcessor) {
}
// NewWakeHandler creates a handler for the wake endpoint.
func NewWakeHandler(agents *agent.Router, token string) *WakeHandler {
return &WakeHandler{agents: agents, token: token}
func NewWakeHandler(agents *agent.Router) *WakeHandler {
return &WakeHandler{agents: agents}
}
// RegisterRoutes registers wake routes on the given mux.
@@ -62,7 +61,7 @@ func (h *WakeHandler) handleWake(w http.ResponseWriter, r *http.Request) {
locale := extractLocale(r)
// Auth + RBAC check (gateway token or API key, operator required for POST)
auth := resolveAuth(r, h.token)
auth := resolveAuth(r)
if !auth.Authenticated {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": i18n.T(locale, i18n.MsgUnauthorized)})
return
+15
View File
@@ -35,6 +35,8 @@ const (
CrossTenantKey contextKey = "goclaw_cross_tenant"
// TenantSlugKey stores the tenant's URL-safe slug for filesystem paths.
TenantSlugKey contextKey = "goclaw_tenant_slug"
// RoleKey is the context key for the caller's permission role (e.g. "admin", "operator", "viewer").
RoleKey contextKey = "goclaw_role"
)
// WithShellDenyGroups returns a new context with shell deny group overrides.
@@ -204,3 +206,16 @@ func TenantSlugFromContext(ctx context.Context) string {
}
return ""
}
// WithRole returns a new context with the caller's permission role.
func WithRole(ctx context.Context, role string) context.Context {
return context.WithValue(ctx, RoleKey, role)
}
// RoleFromContext extracts the permission role from context. Returns "" if not set.
func RoleFromContext(ctx context.Context) string {
if v, ok := ctx.Value(RoleKey).(string); ok {
return v
}
return ""
}