From e66badb803fae21617407f5512ba752fb8d4f1ad Mon Sep 17 00:00:00 2001 From: viettranx Date: Sat, 18 Apr 2026 21:35:46 +0700 Subject: [PATCH] fix(permissions): auto-enrich file_writer metadata on grant from Web UI --- cmd/gateway.go | 9 ++++++- cmd/gateway_methods.go | 4 +-- .../gateway/methods/config_permissions.go | 26 ++++++++++++++++--- internal/http/channel_instances.go | 18 +++++++++++++ 4 files changed, 50 insertions(+), 7 deletions(-) diff --git a/cmd/gateway.go b/cmd/gateway.go index a63293d4..0ebb2a89 100644 --- a/cmd/gateway.go +++ b/cmd/gateway.go @@ -389,7 +389,7 @@ func runGateway() { // Register all RPC methods server.SetLogTee(logTee) - pairingMethods, heartbeatMethods, chatMethods := registerAllMethods(server, agentRouter, pgStores.Sessions, pgStores.Cron, pgStores.Pairing, cfg, cfgPath, workspace, dataDir, msgBus, execApprovalMgr, pgStores.Agents, pgStores.Skills, pgStores.ConfigSecrets, pgStores.Teams, contextFileInterceptor, logTee, pgStores.Heartbeats, pgStores.ConfigPermissions, pgStores.SystemConfigs, pgStores.Tenants, pgStores.SkillTenantCfgs, audioMgr) + pairingMethods, heartbeatMethods, chatMethods, cfgPermsMethods := registerAllMethods(server, agentRouter, pgStores.Sessions, pgStores.Cron, pgStores.Pairing, cfg, cfgPath, workspace, dataDir, msgBus, execApprovalMgr, pgStores.Agents, pgStores.Skills, pgStores.ConfigSecrets, pgStores.Teams, contextFileInterceptor, logTee, pgStores.Heartbeats, pgStores.ConfigPermissions, pgStores.SystemConfigs, pgStores.Tenants, pgStores.SkillTenantCfgs, audioMgr) // Phase 3: Agent hooks RPC methods (hooks.list/create/update/delete/toggle/test/history). if hs, ok := pgStores.Hooks.(hooks.HookStore); ok && hs != nil { @@ -428,6 +428,13 @@ func runGateway() { channelMgr := channels.NewManager(msgBus) deps.channelMgr = channelMgr + // Wire channel member resolver into permission grant paths (WS + HTTP) so + // file_writer grants coming from the Web UI auto-enrich their metadata. + cfgPermsMethods.SetMemberResolver(channelMgr) + if channelInstancesH != nil { + channelInstancesH.SetMemberResolver(channelMgr) + } + // Wire channel sender + tenant checker on message tool (now that channelMgr exists) if t, ok := toolsReg.Get("message"); ok { if cs, ok := t.(tools.ChannelSenderAware); ok { diff --git a/cmd/gateway_methods.go b/cmd/gateway_methods.go index c59d0c99..d994217e 100644 --- a/cmd/gateway_methods.go +++ b/cmd/gateway_methods.go @@ -14,7 +14,7 @@ import ( "github.com/nextlevelbuilder/goclaw/internal/tools" ) -func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore store.SessionStore, cronStore store.CronStore, pairingStore store.PairingStore, cfg *config.Config, cfgPath, workspace, dataDir string, msgBus *bus.MessageBus, execApprovalMgr *tools.ExecApprovalManager, agentStore store.AgentStore, skillStore store.SkillStore, configSecretsStore store.ConfigSecretsStore, teamStore store.TeamStore, contextFileInterceptor *tools.ContextFileInterceptor, logTee *gateway.LogTee, heartbeatStore store.HeartbeatStore, configPermStore store.ConfigPermissionStore, sysConfigStore store.SystemConfigStore, tenantStore store.TenantStore, skillTenantCfgStore store.SkillTenantConfigStore, audioMgr *audio.Manager) (*methods.PairingMethods, *methods.HeartbeatMethods, *methods.ChatMethods) { +func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore store.SessionStore, cronStore store.CronStore, pairingStore store.PairingStore, cfg *config.Config, cfgPath, workspace, dataDir string, msgBus *bus.MessageBus, execApprovalMgr *tools.ExecApprovalManager, agentStore store.AgentStore, skillStore store.SkillStore, configSecretsStore store.ConfigSecretsStore, teamStore store.TeamStore, contextFileInterceptor *tools.ContextFileInterceptor, logTee *gateway.LogTee, heartbeatStore store.HeartbeatStore, configPermStore store.ConfigPermissionStore, sysConfigStore store.SystemConfigStore, tenantStore store.TenantStore, skillTenantCfgStore store.SkillTenantConfigStore, audioMgr *audio.Manager) (*methods.PairingMethods, *methods.HeartbeatMethods, *methods.ChatMethods, *methods.ConfigPermissionsMethods) { router := server.Router() // Phase 1: Core methods @@ -77,5 +77,5 @@ func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore "phase2", []string{"skills", "cron", "heartbeat", "pairing", "usage", "exec_approval", "send"}, ) - return pairingMethods, heartbeatMethods, chatMethods + return pairingMethods, heartbeatMethods, chatMethods, cfgPerms } diff --git a/internal/gateway/methods/config_permissions.go b/internal/gateway/methods/config_permissions.go index 31f65e1e..4e3044e8 100644 --- a/internal/gateway/methods/config_permissions.go +++ b/internal/gateway/methods/config_permissions.go @@ -6,6 +6,7 @@ import ( "log/slog" "github.com/nextlevelbuilder/goclaw/internal/agent" + "github.com/nextlevelbuilder/goclaw/internal/channels" "github.com/nextlevelbuilder/goclaw/internal/gateway" "github.com/nextlevelbuilder/goclaw/internal/i18n" "github.com/nextlevelbuilder/goclaw/internal/store" @@ -14,9 +15,10 @@ import ( // ConfigPermissionsMethods handles config.permissions.* RPC methods. type ConfigPermissionsMethods struct { - permStore store.ConfigPermissionStore - agentStore store.AgentStore - agentRouter *agent.Router // cache-aware agent resolver; nil = DB-only fallback + permStore store.ConfigPermissionStore + agentStore store.AgentStore + agentRouter *agent.Router // cache-aware agent resolver; nil = DB-only fallback + memberResolver channels.MemberResolver // optional — enriches file_writer metadata on grant } func NewConfigPermissionsMethods(ps store.ConfigPermissionStore, as store.AgentStore) *ConfigPermissionsMethods { @@ -28,6 +30,12 @@ func (m *ConfigPermissionsMethods) SetAgentRouter(r *agent.Router) { m.agentRouter = r } +// SetMemberResolver wires a channel member resolver so Grant can auto-enrich +// file_writer metadata when the caller supplies none (e.g. Web UI path). +func (m *ConfigPermissionsMethods) SetMemberResolver(r channels.MemberResolver) { + m.memberResolver = r +} + func (m *ConfigPermissionsMethods) Register(router *gateway.MethodRouter) { router.Register(protocol.MethodConfigPermissionsList, m.handleList) router.Register(protocol.MethodConfigPermissionsGrant, m.handleGrant) @@ -110,6 +118,16 @@ func (m *ConfigPermissionsMethods) handleGrant(ctx context.Context, client *gate } } + // Auto-enrich file_writer metadata for group scopes when caller supplied none. + // Best-effort: failure (bot offline, user left group, channel not supported) + // leaves params.Metadata as-is and the store's own fallback ("{}") applies. + metadata := params.Metadata + if params.ConfigType == store.ConfigTypeFileWriter && channels.IsEmptyWriterMetadata(metadata) { + if enriched, ok := channels.EnrichFileWriterMetadata(ctx, m.memberResolver, params.Scope, params.UserID); ok { + metadata = enriched + } + } + perm := &store.ConfigPermission{ AgentID: agentUUID, Scope: params.Scope, @@ -117,7 +135,7 @@ func (m *ConfigPermissionsMethods) handleGrant(ctx context.Context, client *gate UserID: params.UserID, Permission: params.Permission, GrantedBy: grantedBy, - Metadata: params.Metadata, + Metadata: metadata, } if err := m.permStore.Grant(ctx, perm); err != nil { diff --git a/internal/http/channel_instances.go b/internal/http/channel_instances.go index f5529a8b..b29275c8 100644 --- a/internal/http/channel_instances.go +++ b/internal/http/channel_instances.go @@ -10,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/nextlevelbuilder/goclaw/internal/bus" + "github.com/nextlevelbuilder/goclaw/internal/channels" "github.com/nextlevelbuilder/goclaw/internal/i18n" "github.com/nextlevelbuilder/goclaw/internal/permissions" "github.com/nextlevelbuilder/goclaw/internal/store" @@ -24,6 +25,7 @@ type ChannelInstancesHandler struct { contactStore store.ContactStore tenantStore store.TenantStore msgBus *bus.MessageBus + memberResolver channels.MemberResolver // optional — enriches file_writer metadata on addwriter } // NewChannelInstancesHandler creates a handler for channel instance management endpoints. @@ -31,6 +33,12 @@ func NewChannelInstancesHandler(s store.ChannelInstanceStore, agentStore store.A return &ChannelInstancesHandler{store: s, agentStore: agentStore, configPermStore: configPermStore, contactStore: contactStore, tenantStore: tenantStore, msgBus: msgBus} } +// SetMemberResolver wires a channel member resolver so addwriter can auto-fill +// metadata when the caller supplies neither DisplayName nor Username. +func (h *ChannelInstancesHandler) SetMemberResolver(r channels.MemberResolver) { + h.memberResolver = r +} + // RegisterRoutes registers all channel instance routes on the given mux. func (h *ChannelInstancesHandler) RegisterRoutes(mux *http.ServeMux) { // Channel instance CRUD (reads: viewer+, writes: admin+) @@ -63,6 +71,8 @@ func (h *ChannelInstancesHandler) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /v1/channels/instances/{id}/writers", h.auth(h.handleListWriters)) mux.HandleFunc("POST /v1/channels/instances/{id}/writers", h.adminAuth(h.handleAddWriter)) mux.HandleFunc("DELETE /v1/channels/instances/{id}/writers/{userId}", h.adminAuth(h.handleRemoveWriter)) + // One-shot legacy-row backfill. Admin-only, idempotent, safe to re-run. + mux.HandleFunc("POST /v1/admin/writer-metadata/backfill", h.adminAuth(h.handleBackfillWriterMetadata)) } } @@ -412,6 +422,14 @@ func (h *ChannelInstancesHandler) handleAddWriter(w http.ResponseWriter, r *http return } meta, _ := json.Marshal(map[string]string{"displayName": body.DisplayName, "username": body.Username}) + // Auto-enrich when caller supplied neither display name nor username. + // Best-effort — any resolver error leaves the metadata as-is so the + // store's fallback ({}) applies and the grant still succeeds. + if body.DisplayName == "" && body.Username == "" { + if enriched, ok := channels.EnrichFileWriterMetadata(r.Context(), h.memberResolver, body.GroupID, body.UserID); ok { + meta = enriched + } + } if err := h.configPermStore.Grant(r.Context(), &store.ConfigPermission{ AgentID: agentID, Scope: body.GroupID,