diff --git a/backend/internal/api/controller/asset.go b/backend/internal/api/controller/asset.go index b55b3ff..ca333f3 100644 --- a/backend/internal/api/controller/asset.go +++ b/backend/internal/api/controller/asset.go @@ -19,10 +19,7 @@ import ( ) const ( - kFmtAssetIds = "assetIds-%d" kAuthorizationIds = "authorizationIds" - kNodeIds = "nodeIds" - kAccountIds = "accountIds" ) var ( @@ -126,7 +123,7 @@ func (c *Controller) GetAssets(ctx *gin.Context) { if info { db = db.Select("id", "parent_id", "name", "ip", "protocols", "connectable", "authorization", "resource_id", "access_time_control", - "asset_command_control", "web_config", "gateway_id") + "asset_command_control", "web_config", "gateway_id", "ci_id", "ci_type_id") } doGet(ctx, false, db, config.RESOURCE_ASSET, assetPostHooks...) diff --git a/backend/internal/api/controller/base.go b/backend/internal/api/controller/base.go index fabc36b..f55de31 100644 --- a/backend/internal/api/controller/base.go +++ b/backend/internal/api/controller/base.go @@ -1,8 +1,11 @@ package controller import ( + "bytes" "context" + "encoding/json" "errors" + "io" "net/http" "reflect" "time" @@ -245,10 +248,26 @@ func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType return } + // Read request body for partial update field detection + bodyBytes, err := io.ReadAll(ctx.Request.Body) + if err != nil { + ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}}) + return + } + // Restore body for binding + ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + // Parse body to extract field keys for partial update + var bodyFields map[string]any + if len(bodyBytes) > 0 { + json.Unmarshal(bodyBytes, &bodyFields) + } + if err = ctx.ShouldBindBodyWithJSON(md); err != nil { ctx.AbortWithError(http.StatusBadRequest, &myErrors.ApiError{Code: myErrors.ErrInvalidArgument, Data: map[string]any{"err": err}}) return } + md.SetUpdaterId(currentUser.Uid) for _, hook := range preHooks { @@ -289,27 +308,55 @@ func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType if err = baseService.ExecuteInTransaction(ctx, func(tx *gorm.DB) (err error) { omits := []string{"resource_id", "created_at", "deleted_at"} - selects := []string{"*"} - switch t := any(md).(type) { - case *model.Asset: - if err = service.DefaultAuthService.HandleAuthorization(ctx, tx, model.ACTION_UPDATE, t, nil); err != nil { - handleRemoteErr(ctx, err) - return + var selects []string + + // Build dynamic selects based on fields present in request body + if len(bodyFields) > 0 { + // Define allowed fields per model type + var allowedFields []string + + switch t := any(md).(type) { + case *model.Asset: + if err = service.DefaultAuthService.HandleAuthorization(ctx, tx, model.ACTION_UPDATE, t, nil); err != nil { + handleRemoteErr(ctx, err) + return + } + // Define allowed fields based on auth type + if cast.ToBool(ctx.Value("isAuthWithKey")) { + // API Key auth: Only sync-related fields + allowedFields = []string{"ip", "protocols", "authorization", "parent_id", "comment", "name", "ci_id", "ci_type_id"} + } else { + // Normal auth: All asset fields + allowedFields = []string{"name", "ip", "gateway_id", "protocols", "authorization", "parent_id", + "comment", "connectable", "access_time_control", "asset_command_control", "web_config", "ci_id", "ci_type_id"} + } + case *model.Node: + if err = handleNodeAuthorization(ctx, tx, model.ACTION_UPDATE, t); err != nil { + handleRemoteErr(ctx, err) + return + } + case *model.Account: + // For accounts, allow selective field updates for sensitive data + allowedFields = []string{"name", "account", "account_type", "password", "pk", "phrase"} } - if cast.ToBool(ctx.Value("isAuthWithKey")) { - selects = []string{"ip", "protocols", "authorization"} - } - case *model.Node: - if err = handleNodeAuthorization(ctx, tx, model.ACTION_UPDATE, t); err != nil { - handleRemoteErr(ctx, err) - return - } - case *model.Account: - if cast.ToBool(ctx.Value("isAuthWithKey")) { - selects = []string{"account", "password", "phrase", "pk", "account_type"} + + // Build selects list based on which fields are present in body + if len(allowedFields) > 0 { + for _, field := range allowedFields { + if _, present := bodyFields[field]; present { + selects = append(selects, field) + } + } + // Always update these fields + selects = append(selects, "updated_at", "updater_id") } } + // If no fields specified or no allowed fields matched, update all fields + if len(selects) == 0 { + selects = []string{"*"} + } + if err = tx.Select(selects).Omit(omits...).Save(md).Error; err != nil { return } diff --git a/backend/internal/model/asset.go b/backend/internal/model/asset.go index 294376e..700c61b 100644 --- a/backend/internal/model/asset.go +++ b/backend/internal/model/asset.go @@ -126,6 +126,8 @@ type Asset struct { ParentId int `json:"parent_id" gorm:"column:parent_id"` Ip string `json:"ip" gorm:"column:ip"` Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"` + CIId int `json:"ci_id" gorm:"column:ci_id;index"` + CITypeId int `json:"ci_type_id" gorm:"column:ci_type_id"` GatewayId int `json:"gateway_id" gorm:"column:gateway_id"` Authorization AuthorizationMap `json:"authorization" gorm:"column:authorization;type:text"` AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"` // Deprecated: Use V2 fields below diff --git a/backend/internal/repository/asset.go b/backend/internal/repository/asset.go index 37fda71..e6b7685 100644 --- a/backend/internal/repository/asset.go +++ b/backend/internal/repository/asset.go @@ -63,6 +63,12 @@ func (r *assetRepository) BuildQuery(ctx *gin.Context) (*gorm.DB, error) { func(s string, _ int) int { return cast.ToInt(s) })) } + // Filter by CMDB CI ID + db = dbpkg.FilterEqual(ctx, db, "ci_id") + + // Filter by CMDB CI Type ID + db = dbpkg.FilterEqual(ctx, db, "ci_type_id") + // Sort by name db = db.Order("name") diff --git a/backend/internal/service/asset.go b/backend/internal/service/asset.go index d2fd7e3..eda46b9 100644 --- a/backend/internal/service/asset.go +++ b/backend/internal/service/asset.go @@ -10,6 +10,7 @@ import ( "github.com/veops/oneterm/internal/model" "github.com/veops/oneterm/internal/repository" "github.com/veops/oneterm/internal/schedule" + "github.com/veops/oneterm/internal/sshsrv/icons" "gorm.io/gorm" ) @@ -33,7 +34,22 @@ func (s *AssetService) GetById(ctx context.Context, id int) (*model.Asset, error // PreprocessAssetData preprocesses asset data before saving func (s *AssetService) PreprocessAssetData(asset *model.Asset) { asset.Ip = strings.TrimSpace(asset.Ip) - asset.Protocols = lo.Map(asset.Protocols, func(s string, _ int) string { return strings.TrimSpace(s) }) + + // Normalize protocols: add default ports if not specified + asset.Protocols = lo.Map(asset.Protocols, func(p string, _ int) string { + p = strings.TrimSpace(p) + // If protocol already has port (contains ':'), keep as-is + if strings.Contains(p, ":") { + return p + } + // Otherwise, add default port + if defaultPort := icons.GetDefaultPort(p); defaultPort != "" { + return p + ":" + defaultPort + } + // If no default port available, keep as-is + return p + }) + if asset.Authorization == nil { asset.Authorization = make(model.AuthorizationMap) } diff --git a/backend/internal/sshsrv/icons/icons.go b/backend/internal/sshsrv/icons/icons.go index 9dcc5d6..02f70aa 100644 --- a/backend/internal/sshsrv/icons/icons.go +++ b/backend/internal/sshsrv/icons/icons.go @@ -51,6 +51,10 @@ func GetDefaultPort(protocol string) string { switch protocol { case "ssh": return "22" + case "rdp": + return "3389" + case "vnc": + return "5900" case "mysql": return "3306" case "redis":