fix(permissions): auto-enrich file_writer metadata on grant from Web UI

This commit is contained in:
viettranx committed 2026-04-18 21:35:46 +07:00
1 parent cadca23cab
commit e66badb803
4 files changed
+50 -7

No files matched your search

+8 -1
View File
@@ -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 {
+2 -2
View File
@@ -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
}
+22 -4
View File
@@ -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 {
+18
View File
@@ -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,