mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 14:16:08 +00:00
230 lines
8.5 KiB
Go
230 lines
8.5 KiB
Go
package methods
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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"
|
|
"github.com/nextlevelbuilder/goclaw/pkg/protocol"
|
|
)
|
|
|
|
// 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
|
|
memberResolver channels.MemberResolver // optional — enriches file_writer metadata on grant
|
|
}
|
|
|
|
func NewConfigPermissionsMethods(ps store.ConfigPermissionStore, as store.AgentStore) *ConfigPermissionsMethods {
|
|
return &ConfigPermissionsMethods{permStore: ps, agentStore: as}
|
|
}
|
|
|
|
// SetAgentRouter wires the agent router for cache-aware agent_key resolution.
|
|
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.MethodConfigPermissionsCheck, m.handleCheck)
|
|
router.Register(protocol.MethodConfigPermissionsGrant, m.handleGrant)
|
|
router.Register(protocol.MethodConfigPermissionsRevoke, m.handleRevoke)
|
|
}
|
|
|
|
func (m *ConfigPermissionsMethods) handleList(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
var params struct {
|
|
AgentID string `json:"agentId"`
|
|
ConfigType string `json:"configType"`
|
|
}
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
if params.AgentID == "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, i18n.T(locale, i18n.MsgRequired, "agentId")))
|
|
return
|
|
}
|
|
if params.ConfigType != "" && !store.ValidConfigType(params.ConfigType) {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid configType"))
|
|
return
|
|
}
|
|
|
|
agentUUID, err := resolveAgentUUIDCached(ctx, m.agentRouter, m.agentStore, params.AgentID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid agentId"))
|
|
return
|
|
}
|
|
|
|
perms, err := m.permStore.List(ctx, agentUUID, params.ConfigType, "")
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, configPermInternalErr("list", err)))
|
|
return
|
|
}
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"permissions": perms}))
|
|
}
|
|
|
|
func (m *ConfigPermissionsMethods) handleCheck(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
var params struct {
|
|
AgentID string `json:"agentId"`
|
|
Scope string `json:"scope"`
|
|
ConfigType string `json:"configType"`
|
|
UserID string `json:"userId"`
|
|
}
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
|
|
if errMsg := validateConfigPermissionParams(locale, params.AgentID, params.Scope, params.ConfigType, params.UserID, "allow", false); errMsg != "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, errMsg))
|
|
return
|
|
}
|
|
|
|
agentUUID, err := resolveAgentUUIDCached(ctx, m.agentRouter, m.agentStore, params.AgentID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid agentId"))
|
|
return
|
|
}
|
|
|
|
decision, err := store.CheckConfigPermissionDecision(ctx, m.permStore, agentUUID, params.Scope, params.ConfigType, params.UserID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, configPermInternalErr("check", err)))
|
|
return
|
|
}
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"decision": decision}))
|
|
}
|
|
|
|
func (m *ConfigPermissionsMethods) handleGrant(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
var params struct {
|
|
AgentID string `json:"agentId"`
|
|
Scope string `json:"scope"`
|
|
ConfigType string `json:"configType"`
|
|
UserID string `json:"userId"`
|
|
Permission string `json:"permission"`
|
|
GrantedBy *string `json:"grantedBy,omitempty"`
|
|
Metadata json.RawMessage `json:"metadata,omitempty"`
|
|
}
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
|
|
if errMsg := validateConfigPermissionParams(locale, params.AgentID, params.Scope, params.ConfigType, params.UserID, params.Permission, true); errMsg != "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, errMsg))
|
|
return
|
|
}
|
|
|
|
agentUUID, err := resolveAgentUUIDCached(ctx, m.agentRouter, m.agentStore, params.AgentID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid agentId"))
|
|
return
|
|
}
|
|
|
|
// Auto-fill grantedBy from the caller's identity if not explicitly provided.
|
|
grantedBy := params.GrantedBy
|
|
if grantedBy == nil {
|
|
if caller := store.UserIDFromContext(ctx); caller != "" {
|
|
grantedBy = &caller
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
ConfigType: params.ConfigType,
|
|
UserID: params.UserID,
|
|
Permission: params.Permission,
|
|
GrantedBy: grantedBy,
|
|
Metadata: metadata,
|
|
}
|
|
|
|
if err := m.permStore.Grant(ctx, perm); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, configPermInternalErr("grant", err)))
|
|
return
|
|
}
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
|
}
|
|
|
|
func (m *ConfigPermissionsMethods) handleRevoke(ctx context.Context, client *gateway.Client, req *protocol.RequestFrame) {
|
|
locale := store.LocaleFromContext(ctx)
|
|
var params struct {
|
|
AgentID string `json:"agentId"`
|
|
Scope string `json:"scope"`
|
|
ConfigType string `json:"configType"`
|
|
UserID string `json:"userId"`
|
|
}
|
|
if req.Params != nil {
|
|
json.Unmarshal(req.Params, ¶ms)
|
|
}
|
|
|
|
if errMsg := validateConfigPermissionParams(locale, params.AgentID, params.Scope, params.ConfigType, params.UserID, "allow", false); errMsg != "" {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, errMsg))
|
|
return
|
|
}
|
|
|
|
agentUUID, err := resolveAgentUUIDCached(ctx, m.agentRouter, m.agentStore, params.AgentID)
|
|
if err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInvalidRequest, "invalid agentId"))
|
|
return
|
|
}
|
|
|
|
if err := m.permStore.Revoke(ctx, agentUUID, params.Scope, params.ConfigType, params.UserID); err != nil {
|
|
client.SendResponse(protocol.NewErrorResponse(req.ID, protocol.ErrInternal, configPermInternalErr("revoke", err)))
|
|
return
|
|
}
|
|
|
|
client.SendResponse(protocol.NewOKResponse(req.ID, map[string]any{"ok": true}))
|
|
}
|
|
|
|
func configPermInternalErr(action string, err error) string {
|
|
slog.Error("config.permissions RPC error", "action", action, "error", err)
|
|
return "internal error"
|
|
}
|
|
|
|
func validateConfigPermissionParams(locale, agentID, scope, configType, userID, permission string, validatePermission bool) string {
|
|
switch {
|
|
case agentID == "":
|
|
return i18n.T(locale, i18n.MsgRequired, "agentId")
|
|
case scope == "":
|
|
return i18n.T(locale, i18n.MsgRequired, "scope")
|
|
case configType == "":
|
|
return i18n.T(locale, i18n.MsgRequired, "configType")
|
|
case userID == "":
|
|
return i18n.T(locale, i18n.MsgRequired, "userId")
|
|
case !store.ValidConfigScope(scope):
|
|
return "invalid scope"
|
|
case !store.ValidConfigType(configType):
|
|
return "invalid configType"
|
|
case validatePermission && permission == "":
|
|
return i18n.T(locale, i18n.MsgRequired, "permission")
|
|
case validatePermission && !store.ValidConfigPermission(permission):
|
|
return "invalid permission"
|
|
}
|
|
return ""
|
|
}
|