Files
oneterm/backend/internal/service/command_analyzer.go
2025-07-16 18:11:04 +08:00

266 lines
8.1 KiB
Go

package service
import (
"context"
"regexp"
"strconv"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"go.uber.org/zap"
"github.com/veops/oneterm/internal/model"
"github.com/veops/oneterm/internal/repository"
gsession "github.com/veops/oneterm/internal/session"
"github.com/veops/oneterm/pkg/logger"
)
// CommandAnalyzer handles command analysis for sessions
type CommandAnalyzer struct {
authService IAuthorizationService
}
// NewCommandAnalyzer creates a new command analyzer
func NewCommandAnalyzer() *CommandAnalyzer {
return &CommandAnalyzer{
authService: DefaultAuthService,
}
}
// AnalyzeSessionCommands analyzes and builds the final command list for a session
// This combines asset-level and authorization-level command controls
func (ca *CommandAnalyzer) AnalyzeSessionCommands(ctx *gin.Context, sess *gsession.Session) ([]*model.Command, error) {
// Get all available commands from cache
allCommands, err := repository.GetAllFromCacheDb(ctx, model.DefaultCommand)
if err != nil {
logger.L().Error("Failed to get commands from cache", zap.Error(err))
return nil, err
}
// Filter enabled commands
enabledCommands := lo.Filter(allCommands, func(cmd *model.Command, _ int) bool {
return cmd.Enable
})
// Analyze asset-level command control
assetCommands := ca.analyzeAssetCommands(sess.Session.Asset, enabledCommands)
// Analyze authorization-level command control
authCommands, err := ca.analyzeAuthorizationCommands(ctx, sess, enabledCommands)
if err != nil {
logger.L().Error("Failed to analyze authorization commands", zap.Error(err))
// Continue with asset-level commands only
authCommands = []*model.Command{}
}
// Merge and deduplicate command lists
finalCommands := ca.mergeCommands(assetCommands, authCommands)
// Compile regex patterns for performance
for _, cmd := range finalCommands {
if cmd.IsRe {
if re, err := regexp.Compile(cmd.Cmd); err == nil {
cmd.Re = re
} else {
logger.L().Warn("Invalid regex pattern in command",
zap.String("cmd", cmd.Cmd),
zap.Int("id", cmd.Id),
zap.Error(err))
}
}
}
logger.L().Info("Command analysis completed",
zap.String("sessionId", sess.SessionId),
zap.Int("assetCommands", len(assetCommands)),
zap.Int("authCommands", len(authCommands)),
zap.Int("finalCommands", len(finalCommands)))
return finalCommands, nil
}
// analyzeAssetCommands analyzes asset-level command controls from V2 system
func (ca *CommandAnalyzer) analyzeAssetCommands(asset *model.Asset, allCommands []*model.Command) []*model.Command {
var result []*model.Command
// V2 asset command control
if asset.AssetCommandControl != nil && asset.AssetCommandControl.Enabled {
var v2Commands []*model.Command
// Process direct command IDs
if len(asset.AssetCommandControl.CmdIds) > 0 {
cmdMap := make(map[int]*model.Command)
for _, cmd := range allCommands {
cmdMap[cmd.Id] = cmd
}
for _, cmdId := range asset.AssetCommandControl.CmdIds {
if cmd, exists := cmdMap[cmdId]; exists {
v2Commands = append(v2Commands, cmd)
}
}
}
// Process command template IDs
if len(asset.AssetCommandControl.TemplateIds) > 0 {
templateCommands := ca.expandCommandTemplates(asset.AssetCommandControl.TemplateIds, allCommands)
v2Commands = append(v2Commands, templateCommands...)
}
// All configured commands are intercepted
result = append(result, v2Commands...)
logger.L().Debug("Asset V2 command control applied",
zap.Int("assetId", asset.Id),
zap.Int("cmdCount", len(v2Commands)))
}
return lo.UniqBy(result, func(cmd *model.Command) int { return cmd.Id })
}
// analyzeAuthorizationCommands analyzes authorization-level command controls from V2 rules
func (ca *CommandAnalyzer) analyzeAuthorizationCommands(ctx *gin.Context, sess *gsession.Session, allCommands []*model.Command) ([]*model.Command, error) {
// Get user's authorized V2 rules
authV2ResourceIds, err := ca.getAuthorizedV2ResourceIds(ctx)
if err != nil {
return nil, err
}
if len(authV2ResourceIds) == 0 {
return []*model.Command{}, nil
}
// Get V2 rules that apply to this session
authV2Service := NewAuthorizationV2Service()
rules, err := authV2Service.repo.GetByResourceIds(ctx, authV2ResourceIds)
if err != nil {
return nil, err
}
var result []*model.Command
// Analyze each applicable rule
for _, rule := range rules {
if !rule.Enabled {
continue
}
// Check if rule matches this session (simplified matching)
if ca.ruleMatchesSession(rule, sess) {
ruleCommands := ca.extractCommandsFromRule(rule, allCommands)
result = append(result, ruleCommands...)
}
}
return lo.UniqBy(result, func(cmd *model.Command) int { return cmd.Id }), nil
}
// ruleMatchesSession checks if a V2 rule matches the current session (simplified)
func (ca *CommandAnalyzer) ruleMatchesSession(rule *model.AuthorizationV2, sess *gsession.Session) bool {
// Quick check for asset selector
if rule.AssetSelector.Type == model.SelectorTypeIds {
assetIds := lo.FilterMap(rule.AssetSelector.Values, func(v string, _ int) (int, bool) {
if id, err := strconv.Atoi(v); err == nil {
return id, true
}
return 0, false
})
if !lo.Contains(assetIds, sess.AssetId) {
return false
}
} else if rule.AssetSelector.Type != model.SelectorTypeAll {
// For regex/tags selectors, we'd need more complex matching
// For now, assume they match (could be optimized later)
}
// Quick check for account selector
if rule.AccountSelector.Type == model.SelectorTypeIds {
accountIds := lo.FilterMap(rule.AccountSelector.Values, func(v string, _ int) (int, bool) {
if id, err := strconv.Atoi(v); err == nil {
return id, true
}
return 0, false
})
if !lo.Contains(accountIds, sess.AccountId) {
return false
}
}
return true
}
// extractCommandsFromRule extracts commands from a V2 authorization rule
func (ca *CommandAnalyzer) extractCommandsFromRule(rule *model.AuthorizationV2, allCommands []*model.Command) []*model.Command {
var result []*model.Command
// Process direct command IDs
if len(rule.AccessControl.CmdIds) > 0 {
cmdMap := make(map[int]*model.Command)
for _, cmd := range allCommands {
cmdMap[cmd.Id] = cmd
}
for _, cmdId := range rule.AccessControl.CmdIds {
if cmd, exists := cmdMap[cmdId]; exists {
result = append(result, cmd)
}
}
}
// Process command template IDs
if len(rule.AccessControl.TemplateIds) > 0 {
templateCommands := ca.expandCommandTemplates(rule.AccessControl.TemplateIds, allCommands)
result = append(result, templateCommands...)
}
// All configured commands are intercepted
return result
}
// expandCommandTemplates expands command template IDs to actual commands
func (ca *CommandAnalyzer) expandCommandTemplates(templateIds []int, allCommands []*model.Command) []*model.Command {
// Get command templates from database
commandTemplateService := NewCommandTemplateService()
ctx := context.Background()
var expandedCommands []*model.Command
for _, templateId := range templateIds {
template, err := commandTemplateService.GetCommandTemplate(ctx, templateId)
if err != nil {
logger.L().Warn("Failed to get command template",
zap.Int("templateId", templateId),
zap.Error(err))
continue
}
if template == nil {
continue
}
// Get commands from template
templateCmdIds := lo.Map(template.CmdIds, func(id int, _ int) int { return id })
templateCommands := lo.Filter(allCommands, func(cmd *model.Command, _ int) bool {
return lo.Contains(templateCmdIds, cmd.Id)
})
expandedCommands = append(expandedCommands, templateCommands...)
}
return expandedCommands
}
// mergeCommands merges and deduplicates command lists
func (ca *CommandAnalyzer) mergeCommands(assetCommands, authCommands []*model.Command) []*model.Command {
// Combine all commands
allCommands := append(assetCommands, authCommands...)
// Deduplicate by ID
return lo.UniqBy(allCommands, func(cmd *model.Command) int { return cmd.Id })
}
// getAuthorizedV2ResourceIds gets V2 authorization rule resource IDs that user has permission to
func (ca *CommandAnalyzer) getAuthorizedV2ResourceIds(ctx *gin.Context) ([]int, error) {
return ca.authService.(*AuthorizationService).getAuthorizedV2ResourceIds(ctx)
}