mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 20:17:20 +00:00
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:
1 parent
5b749faa35
commit
e1cb5c411b
37 files changed
+201
-207
No files matched your search
+17
-16
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
Reference in new issue
Block a user