mirror of
https://github.com/veops/oneterm.git
synced 2025-11-01 03:12:39 +08:00
266 lines
8.1 KiB
Go
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)
|
|
}
|