This commit is contained in:
LH_R
2025-08-06 19:34:14 +08:00
8 changed files with 167 additions and 74 deletions

View File

@@ -7,6 +7,7 @@ import (
"github.com/samber/lo"
"github.com/spf13/cast"
"github.com/veops/oneterm/internal/acl"
"github.com/veops/oneterm/internal/model"
"github.com/veops/oneterm/internal/service"
"github.com/veops/oneterm/pkg/config"
@@ -41,6 +42,20 @@ var (
func(ctx *gin.Context, data []*model.Account) {
accountService.DecryptSensitiveData(data)
},
// Filter sensitive fields for non-admin users
func(ctx *gin.Context, data []*model.Account) {
info := cast.ToBool(ctx.Query("info"))
if !info {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
for _, account := range data {
account.Password = ""
account.Pk = ""
account.Phrase = ""
}
}
}
},
}
accountDcs = []deleteCheck{
@@ -116,7 +131,7 @@ func (c *Controller) GetAccounts(ctx *gin.Context) {
db = db.Select("id", "name", "account")
}
doGet(ctx, !info, db, config.RESOURCE_ACCOUNT, accountPostHooks...)
doGet(ctx, false, db, config.RESOURCE_ACCOUNT, accountPostHooks...)
}
// GetAccountIdsByAuthorization gets account IDs by authorization

View File

@@ -124,7 +124,9 @@ func (c *Controller) GetAssets(ctx *gin.Context) {
// Apply info mode settings
if info {
db = db.Select("id", "parent_id", "name", "ip", "protocols", "connectable", "authorization", "resource_id", "access_time_control", "asset_command_control", "web_config")
db = db.Select("id", "parent_id", "name", "ip", "protocols",
"connectable", "authorization", "resource_id", "access_time_control",
"asset_command_control", "web_config", "gateway_id")
}
doGet(ctx, false, db, config.RESOURCE_ASSET, assetPostHooks...)

View File

@@ -106,7 +106,7 @@ func (c *Controller) GetCommands(ctx *gin.Context) {
}
if info && !acl.IsAdmin(currentUser) {
commandIds, err := commandService.GetAuthorizedCommandIds(ctx, currentUser)
commandIds, err := commandService.GetAuthorizedCommandIds(ctx)
if err != nil {
handleRemoteErr(ctx, err)
return

View File

@@ -2,6 +2,7 @@ package protocols
import (
"fmt"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -51,7 +52,13 @@ func ConnectGuacd(ctx *gin.Context, sess *gsession.Session, asset *model.Asset,
permissions.AllowFileDownload = batchResult.IsAllowed(model.ActionFileDownload)
}
t, err := guacd.NewTunnel("", sess.SessionId, w, h, dpi, sess.Protocol, asset, account, gateway, permissions)
// Clean protocol parameter - remove port number if present for guacd compatibility
cleanProtocol := sess.Protocol
if strings.Contains(sess.Protocol, ":") {
cleanProtocol = strings.Split(sess.Protocol, ":")[0]
}
t, err := guacd.NewTunnel("", sess.SessionId, w, h, dpi, cleanProtocol, asset, account, gateway, permissions)
if err != nil {
logger.L().Error("guacd tunnel failed", zap.Error(err))
return

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@@ -22,7 +23,6 @@ type AccountRepository interface {
AttachAssetCount(ctx context.Context, accounts []*model.Account) error
CheckAssetDependencies(ctx context.Context, id int) (string, error)
BuildQuery(ctx *gin.Context) *gorm.DB
FilterByAssetIds(db *gorm.DB, assetIds []int) *gorm.DB
GetAccountIdsByAuthorization(ctx context.Context, assetIds []int, authorizationIds []int) ([]int, error)
}
@@ -55,60 +55,121 @@ func (r *accountRepository) BuildQuery(ctx *gin.Context) *gorm.DB {
return db
}
// FilterByAssetIds filters accounts by related asset IDs
func (r *accountRepository) FilterByAssetIds(db *gorm.DB, assetIds []int) *gorm.DB {
if len(assetIds) == 0 {
return db.Where("0 = 1") // Return empty result if no asset IDs
}
// Query account IDs associated with specified assets
subQuery := dbpkg.DB.Model(&model.Authorization{}).
Select("account_id").
Where("asset_id IN ?", assetIds).
Group("account_id")
return db.Where("id IN (?)", subQuery)
}
// AttachAssetCount attaches asset count to accounts
// AttachAssetCount attaches asset count to accounts using V2 authorization system
func (r *accountRepository) AttachAssetCount(ctx context.Context, accounts []*model.Account) error {
acs := make([]*model.AccountCount, 0)
if err := dbpkg.DB.
Model(&model.Authorization{}).
Select("account_id AS id, COUNT(*) as count").
Group("account_id").
Where("account_id IN ?", lo.Map(accounts, func(d *model.Account, _ int) int { return d.Id })).
Find(&acs).
Error; err != nil {
// Get account IDs to filter
accountIds := lo.Map(accounts, func(account *model.Account, _ int) int { return account.Id })
// Get all V2 authorization rules where both account and asset selectors are 'ids' type
// and account selector contains any of the target account IDs
var rules []*model.AuthorizationV2
if err := dbpkg.DB.Model(&model.AuthorizationV2{}).
Where("enabled = ? AND JSON_EXTRACT(account_selector, '$.type') = ? AND JSON_EXTRACT(asset_selector, '$.type') = ?",
true, "ids", "ids").
Find(&rules).Error; err != nil {
return err
}
m := lo.SliceToMap(acs, func(ac *model.AccountCount) (int, int64) { return ac.Id, ac.Count })
for _, d := range accounts {
d.AssetCount = m[d.Id]
// Count assets for each account
accountAssetCounts := make(map[int]int64)
// Filter rules that contain any of the target account IDs
filteredRules := lo.Filter(rules, func(rule *model.AuthorizationV2, _ int) bool {
ruleAccountIds := lo.Map(rule.AccountSelector.Values, func(value string, _ int) int {
if id, err := strconv.Atoi(value); err == nil {
return id
}
return -1
})
// Check if any account ID in the rule matches our target account IDs
for _, ruleAccountId := range ruleAccountIds {
if lo.Contains(accountIds, ruleAccountId) {
return true
}
}
return false
})
for _, rule := range filteredRules {
// Extract account IDs from account selector
ruleAccountIds := lo.FilterMap(rule.AccountSelector.Values, func(value string, _ int) (int, bool) {
if id, err := strconv.Atoi(value); err == nil {
return id, true
}
return 0, false
})
// Extract asset IDs from asset selector
ruleAssetIds := lo.FilterMap(rule.AssetSelector.Values, func(value string, _ int) (int, bool) {
if id, err := strconv.Atoi(value); err == nil {
return id, true
}
return 0, false
})
// Count assets for each account in this rule
for _, accountId := range ruleAccountIds {
accountAssetCounts[accountId] += int64(len(ruleAssetIds))
}
}
// Apply counts to accounts
for _, account := range accounts {
account.AssetCount = accountAssetCounts[account.Id]
}
return nil
}
// CheckAssetDependencies checks if account has dependent assets
// CheckAssetDependencies checks if account has dependent assets using V2 authorization system
func (r *accountRepository) CheckAssetDependencies(ctx context.Context, id int) (string, error) {
var assetName string
err := dbpkg.DB.
Model(model.DefaultAsset).
Select("name").
Where("id = (?)", dbpkg.DB.Model(&model.Authorization{}).Select("asset_id").Where("account_id = ?", id).Limit(1)).
First(&assetName).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
if err != nil {
// Get all V2 authorization rules where both account and asset selectors are 'ids' type
var rules []*model.AuthorizationV2
if err := dbpkg.DB.Model(&model.AuthorizationV2{}).
Where("enabled = ? AND JSON_EXTRACT(account_selector, '$.type') = ? AND JSON_EXTRACT(asset_selector, '$.type') = ?",
true, "ids", "ids").
Find(&rules).Error; err != nil {
return "", err
}
return assetName, errors.New("account has dependent assets")
// Check if any rule contains this account ID
for _, rule := range rules {
// Extract account IDs from account selector
ruleAccountIds := lo.FilterMap(rule.AccountSelector.Values, func(value string, _ int) (int, bool) {
if accountId, err := strconv.Atoi(value); err == nil {
return accountId, true
}
return 0, false
})
// Check if this account ID is in the rule
if lo.Contains(ruleAccountIds, id) {
// Extract asset IDs from asset selector
ruleAssetIds := lo.FilterMap(rule.AssetSelector.Values, func(value string, _ int) (int, bool) {
if assetId, err := strconv.Atoi(value); err == nil {
return assetId, true
}
return 0, false
})
// If there are assets in this rule, return the first asset name
if len(ruleAssetIds) > 0 {
var assetName string
err := dbpkg.DB.
Model(model.DefaultAsset).
Select("name").
Where("id = ?", ruleAssetIds[0]).
First(&assetName).
Error
if err == nil {
return assetName, errors.New("account has dependent assets")
}
}
}
}
return "", nil
}
// GetAccountIdsByAuthorization gets account IDs by authorization and asset IDs

View File

@@ -70,11 +70,6 @@ func (s *AccountService) BuildQuery(ctx *gin.Context) *gorm.DB {
return s.repo.BuildQuery(ctx)
}
// FilterByAssetIds filters accounts by related asset IDs
func (s *AccountService) FilterByAssetIds(db *gorm.DB, assetIds []int) *gorm.DB {
return s.repo.FilterByAssetIds(db, assetIds)
}
// GetAccountIdsByAuthorization gets account IDs by authorization
func (s *AccountService) GetAccountIdsByAuthorization(ctx context.Context, assetIds []int, authorizationIds []int) ([]int, error) {
return s.repo.GetAccountIdsByAuthorization(ctx, assetIds, authorizationIds)
@@ -92,14 +87,20 @@ func (s *AccountService) BuildQueryWithAuthorization(ctx *gin.Context) (*gorm.DB
return db, nil
}
// Apply V2 authorization filter: get authorized asset IDs using V2 system
// Apply V2 authorization filter: get authorized account IDs using V2 system
authV2Service := NewAuthorizationV2Service()
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
_, _, accountIds, err := authV2Service.GetAuthorizationScopeByACL(ctx)
if err != nil {
return nil, err
}
// Use the same filtering logic as before, but with V2 authorized assets
// This maintains the original logic: find accounts that can access the authorized assets
return s.FilterByAssetIds(db, assetIds), nil
// Filter by authorized account IDs at database level (much more efficient)
if len(accountIds) == 0 {
// No access to any accounts
db = db.Where("1 = 0") // Returns empty result set efficiently
} else {
db = db.Where("id IN ?", accountIds)
}
return db, nil
}

View File

@@ -8,10 +8,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"github.com/veops/oneterm/internal/acl"
"github.com/veops/oneterm/internal/model"
"github.com/veops/oneterm/internal/repository"
"github.com/veops/oneterm/pkg/config"
dbpkg "github.com/veops/oneterm/pkg/db"
"gorm.io/gorm"
)
@@ -69,30 +67,26 @@ func (s *CommandService) BuildQuery(ctx *gin.Context) (*gorm.DB, error) {
return db, nil
}
// GetAuthorizedCommandIds gets command IDs that the user is authorized to access
func (s *CommandService) GetAuthorizedCommandIds(ctx context.Context, currentUser interface{}) ([]int, error) {
user, ok := currentUser.(acl.Session)
if !ok {
return nil, fmt.Errorf("invalid user type")
}
rs, err := acl.GetRoleResources(ctx, user.GetRid(), config.RESOURCE_AUTHORIZATION)
// GetAuthorizedCommandIds gets command IDs that the user is authorized to access using V2 authorization system
func (s *CommandService) GetAuthorizedCommandIds(ctx *gin.Context) ([]int, error) {
// Use V2 authorization system to get authorized asset IDs
authV2Service := NewAuthorizationV2Service()
_, assetIds, _, err := authV2Service.GetAuthorizationScopeByACL(ctx)
if err != nil {
return nil, err
}
// Get asset IDs from authorization
sub := dbpkg.DB.
Model(&model.Authorization{}).
Select("DISTINCT asset_id").
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
// No authorized assets means no authorized commands
if len(assetIds) == 0 {
return []int{}, nil
}
// Get command IDs from assets
// Get command IDs from authorized assets
cmdIds := make([]model.Slice[int], 0)
if err = dbpkg.DB.
Model(model.DefaultAsset).
Select("cmd_ids").
Where("id IN (?)", sub).
Where("id IN ?", assetIds).
Find(&cmdIds).
Error; err != nil {
return nil, err

View File

@@ -146,6 +146,12 @@ func GetReplay(sessionID string) (io.ReadCloser, error) {
return file, nil
}
// Try RDP format (no extension) - guacd saves RDP recordings without .cast extension
rdpFilePath := filepath.Join(replayDir, sessionID)
if file, err := os.Open(rdpFilePath); err == nil {
return file, nil
}
// Search in date hierarchy directories (directly under base_path)
entries, err := os.ReadDir(replayDir)
if err != nil {
@@ -158,10 +164,17 @@ func GetReplay(sessionID string) (io.ReadCloser, error) {
if entry.IsDir() {
// Check if directory name looks like a date (YYYY-MM-DD)
if len(entry.Name()) == 10 && entry.Name()[4] == '-' && entry.Name()[7] == '-' {
// Try SSH format (.cast extension)
filePath := filepath.Join(replayDir, entry.Name(), fmt.Sprintf("%s.cast", sessionID))
if file, err := os.Open(filePath); err == nil {
return file, nil
}
// Try RDP format (no extension) in date directories
rdpFilePath := filepath.Join(replayDir, entry.Name(), sessionID)
if file, err := os.Open(rdpFilePath); err == nil {
return file, nil
}
}
}
}