mirror of
https://github.com/veops/oneterm.git
synced 2025-10-06 15:57:04 +08:00
feat(backend): Add Node authorization V2 support with auto-migration
This commit is contained in:
@@ -42,9 +42,6 @@ func initDB() {
|
||||
logger.L().Fatal("Failed to drop index", zap.Error(err))
|
||||
}
|
||||
|
||||
acl.MigrateNode()
|
||||
acl.MigrateCommand()
|
||||
|
||||
gsession.InitSessionCleanup()
|
||||
}
|
||||
|
||||
@@ -62,6 +59,9 @@ func initServices() {
|
||||
if err := timeTemplateService.InitializeBuiltInTemplates(ctx); err != nil {
|
||||
logger.L().Error("Failed to initialize built-in time templates", zap.Error(err))
|
||||
}
|
||||
|
||||
acl.MigrateNode()
|
||||
acl.MigrateCommand()
|
||||
}
|
||||
|
||||
func initStorage() error {
|
||||
|
@@ -108,11 +108,15 @@ func doCreate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType
|
||||
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = handleAuthorization(ctx, tx, model.ACTION_CREATE, t, nil); err != nil {
|
||||
if err = service.DefaultAuthService.HandleAuthorization(ctx, tx, model.ACTION_CREATE, t, nil); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
case *model.Node:
|
||||
if err = handleNodeAuthorization(ctx, tx, model.ACTION_CREATE, t); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
if err = acl.UpdateResource(ctx, currentUser.GetUid(), resourceId, map[string]string{"name": cast.ToString(md.GetId())}); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
@@ -192,7 +196,12 @@ func doDelete[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType
|
||||
if err = baseService.ExecuteInTransaction(ctx, func(tx *gorm.DB) (err error) {
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = handleAuthorization(ctx, tx, model.ACTION_DELETE, t, nil, nil); err != nil {
|
||||
if err = service.DefaultAuthService.HandleAuthorization(ctx, tx, model.ACTION_DELETE, t, nil); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
case *model.Node:
|
||||
if err = handleNodeAuthorization(ctx, tx, model.ACTION_DELETE, t); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
@@ -283,13 +292,18 @@ func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType
|
||||
selects := []string{"*"}
|
||||
switch t := any(md).(type) {
|
||||
case *model.Asset:
|
||||
if err = handleAuthorization(ctx, tx, model.ACTION_UPDATE, t, nil); err != nil {
|
||||
if err = service.DefaultAuthService.HandleAuthorization(ctx, tx, model.ACTION_UPDATE, t, nil); err != nil {
|
||||
handleRemoteErr(ctx, err)
|
||||
return
|
||||
}
|
||||
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"}
|
||||
@@ -519,3 +533,7 @@ func handleAcl[T any](ctx *gin.Context, dbFind *gorm.DB, resourceType string) (d
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handleNodeAuthorization(ctx *gin.Context, tx *gorm.DB, action int, node *model.Node, auths ...*model.Authorization) error {
|
||||
return service.DefaultAuthService.HandleAuthorizationV2(ctx, tx, action, nil, node, auths...)
|
||||
}
|
||||
|
@@ -93,7 +93,7 @@ func (c *Controller) GetNodes(ctx *gin.Context) {
|
||||
|
||||
// Apply info mode settings
|
||||
if info {
|
||||
db = db.Select("id", "parent_id", "name")
|
||||
db = db.Select("id", "parent_id", "name", "authorization")
|
||||
}
|
||||
|
||||
if recursive {
|
||||
|
@@ -16,6 +16,7 @@ const (
|
||||
type AccountAuthorization struct {
|
||||
Rids Slice[int] `json:"rids"` // Role IDs for ACL system
|
||||
Permissions *AuthPermissions `json:"permissions"` // V2 permissions (connect, file_upload, etc.)
|
||||
RuleId int `json:"rule_id"` // V2 authorization rule ID for tracking
|
||||
}
|
||||
|
||||
// AuthorizationMap is a custom type that handles V1 to V2 authorization format conversion
|
||||
|
@@ -15,7 +15,7 @@ type Node struct {
|
||||
Name string `json:"name" gorm:"column:name"`
|
||||
Comment string `json:"comment" gorm:"column:comment"`
|
||||
ParentId int `json:"parent_id" gorm:"column:parent_id"`
|
||||
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization;type:text"`
|
||||
Authorization AuthorizationMap `json:"authorization" gorm:"column:authorization;type:text"`
|
||||
AccessAuth AccessAuth `json:"access_auth" gorm:"embedded;column:access_auth"`
|
||||
Protocols Slice[string] `json:"protocols" gorm:"column:protocols;type:text"`
|
||||
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -40,12 +41,20 @@ func InitAuthorizationService() {
|
||||
// Perform V1 to V2 migration if needed
|
||||
migrationService := NewAuthorizationMigrationService(dbpkg.DB, repo, v2Repo)
|
||||
ctx := context.Background()
|
||||
|
||||
// Migrate Asset authorization
|
||||
if err := migrationService.MigrateV1ToV2(ctx); err != nil {
|
||||
logger.L().Error("Failed to migrate V1 authorization rules to V2", zap.Error(err))
|
||||
logger.L().Error("Failed to migrate Asset authorization rules from V1 to V2", zap.Error(err))
|
||||
// Continue with service initialization even if migration fails
|
||||
// This allows the system to start with existing V2 rules
|
||||
}
|
||||
|
||||
// Migrate Node authorization
|
||||
if err := MigrateNodeAuthorization(); err != nil {
|
||||
logger.L().Error("Failed to migrate Node authorization rules from V1 to V2", zap.Error(err))
|
||||
// Continue with service initialization even if migration fails
|
||||
}
|
||||
|
||||
DefaultAuthService = NewAuthorizationService(repo, dbpkg.DB, matcher) // Use V2 by default
|
||||
}
|
||||
|
||||
@@ -60,6 +69,7 @@ type IAuthorizationService interface {
|
||||
HasAuthorization(ctx *gin.Context, sess *gsession.Session) (bool, error)
|
||||
GetAuthsByAsset(ctx context.Context, asset *model.Asset) ([]*model.Authorization, error)
|
||||
HandleAuthorization(ctx context.Context, tx *gorm.DB, action int, asset *model.Asset, auths ...*model.Authorization) error
|
||||
HandleAuthorizationV2(ctx context.Context, tx *gorm.DB, action int, asset *model.Asset, node *model.Node, auths ...*model.Authorization) error
|
||||
GetNodeAssetAccountIdsByAction(ctx context.Context, action string) (nodeIds, assetIds, accountIds []int, err error)
|
||||
GetAuthorizationIds(ctx *gin.Context) ([]*model.AuthorizationIds, error)
|
||||
|
||||
@@ -229,12 +239,18 @@ func (s *AuthorizationService) GetAuthsByAsset(ctx context.Context, asset *model
|
||||
|
||||
// HandleAuthorization handles authorization operations
|
||||
func (s *AuthorizationService) HandleAuthorization(ctx context.Context, tx *gorm.DB, action int, asset *model.Asset, auths ...*model.Authorization) (err error) {
|
||||
return s.HandleAuthorizationV2(ctx, tx, action, asset, nil, auths...)
|
||||
}
|
||||
|
||||
// HandleAuthorizationV2 handles authorization operations for both Asset and Node
|
||||
func (s *AuthorizationService) HandleAuthorizationV2(ctx context.Context, tx *gorm.DB, action int, asset *model.Asset, node *model.Node, auths ...*model.Authorization) (err error) {
|
||||
defer repository.DeleteAllFromCacheDb(ctx, model.DefaultAuthorization)
|
||||
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
|
||||
eg := &errgroup.Group{}
|
||||
|
||||
// Handle Asset authorization
|
||||
if asset != nil && asset.Id > 0 {
|
||||
switch action {
|
||||
case model.ACTION_CREATE:
|
||||
@@ -258,6 +274,30 @@ func (s *AuthorizationService) HandleAuthorization(ctx context.Context, tx *gorm
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Node authorization
|
||||
if node != nil && node.Id > 0 {
|
||||
switch action {
|
||||
case model.ACTION_CREATE:
|
||||
// V2: Create authorization rules for node
|
||||
err = s.createV2AuthorizationRulesForNode(ctx, tx, node, currentUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case model.ACTION_DELETE:
|
||||
// V2: Delete authorization rules for this node
|
||||
err = s.deleteV2AuthorizationRulesForNode(ctx, tx, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case model.ACTION_UPDATE:
|
||||
// V2: Update authorization rules for this node
|
||||
err = s.updateV2AuthorizationRulesForNode(ctx, tx, node, currentUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle individual authorization records (V1 compatibility)
|
||||
for _, a := range lo.Filter(auths, func(item *model.Authorization, _ int) bool { return item != nil }) {
|
||||
auth := a
|
||||
@@ -490,7 +530,30 @@ func (s *AuthorizationService) createV2AuthorizationRulesForAsset(ctx context.Co
|
||||
}
|
||||
|
||||
for accountId, authData := range asset.Authorization {
|
||||
// Create a V2 authorization rule for this asset-account combination
|
||||
// Check if there's already an existing V2 rule for this asset-account combination
|
||||
// This handles cases where rules might exist from previous operations or historical data
|
||||
existingRule, err := s.findExistingV2Rule(tx, asset.Id, accountId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing V2 rule: %w", err)
|
||||
}
|
||||
|
||||
if existingRule != nil {
|
||||
// Found existing rule (either by JSON query or by name), update it instead of creating new one
|
||||
authDataCopy := authData
|
||||
if err := s.updateV2AuthorizationRuleById(ctx, tx, existingRule.Id, &authDataCopy, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to update existing V2 rule %d for account %d: %w", existingRule.Id, accountId, err)
|
||||
}
|
||||
|
||||
// Set the found RuleId in the asset.Authorization field
|
||||
authDataCopy.RuleId = existingRule.Id
|
||||
asset.Authorization[accountId] = authDataCopy
|
||||
|
||||
// Update the asset's authorization field in database to include the rule ID
|
||||
if err := tx.Model(asset).Update("authorization", asset.Authorization).Error; err != nil {
|
||||
return fmt.Errorf("failed to update asset authorization with existing rule ID: %w", err)
|
||||
}
|
||||
} else {
|
||||
// No existing rule found, create new one
|
||||
rule := &model.AuthorizationV2{
|
||||
Name: fmt.Sprintf("Asset-%d-Account-%d", asset.Id, accountId),
|
||||
Description: fmt.Sprintf("Auto-generated rule for asset %s and account %d", asset.Name, accountId),
|
||||
@@ -534,7 +597,24 @@ func (s *AuthorizationService) createV2AuthorizationRulesForAsset(ctx context.Co
|
||||
// Grant permissions to roles
|
||||
if len(authData.Rids) > 0 {
|
||||
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), authData.Rids, resourceId, []string{acl.READ}); err != nil {
|
||||
return fmt.Errorf("failed to grant role permissions: %w", err)
|
||||
// Log error but continue - ACL resource was created successfully
|
||||
logger.L().Error("Failed to grant role permissions during rule creation",
|
||||
zap.Int("assetId", asset.Id),
|
||||
zap.Int("accountId", accountId),
|
||||
zap.Int("resourceId", resourceId),
|
||||
zap.Ints("rids", authData.Rids),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Important: Update the asset.Authorization field with the rule ID
|
||||
authDataCopy := authData
|
||||
authDataCopy.RuleId = rule.Id
|
||||
asset.Authorization[accountId] = authDataCopy
|
||||
|
||||
// Update the asset's authorization field in database to include the rule ID
|
||||
if err := tx.Model(asset).Update("authorization", asset.Authorization).Error; err != nil {
|
||||
return fmt.Errorf("failed to update asset authorization with rule ID: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -544,23 +624,17 @@ func (s *AuthorizationService) createV2AuthorizationRulesForAsset(ctx context.Co
|
||||
|
||||
// deleteV2AuthorizationRulesForAsset deletes V2 authorization rules for an asset
|
||||
func (s *AuthorizationService) deleteV2AuthorizationRulesForAsset(ctx context.Context, tx *gorm.DB, asset *model.Asset) error {
|
||||
// Find all V2 rules that target this specific asset
|
||||
var rules []*model.AuthorizationV2
|
||||
if err := tx.Where("asset_selector->>'$.values' LIKE ?", fmt.Sprintf("%%\"%d\"%%", asset.Id)).Find(&rules).Error; err != nil {
|
||||
return fmt.Errorf("failed to find V2 rules for asset: %w", err)
|
||||
// Delete rules based on RuleId stored in asset.Authorization field
|
||||
for accountId, authData := range asset.Authorization {
|
||||
if authData.RuleId > 0 {
|
||||
if err := s.deleteV2AuthorizationRuleById(ctx, tx, authData.RuleId); err != nil {
|
||||
// Log error but continue with other deletions
|
||||
logger.L().Error("Failed to delete V2 rule for asset",
|
||||
zap.Int("assetId", asset.Id),
|
||||
zap.Int("accountId", accountId),
|
||||
zap.Int("ruleId", authData.RuleId),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
// Delete each rule and its ACL resource
|
||||
for _, rule := range rules {
|
||||
// Delete ACL resource
|
||||
if err := acl.DeleteResource(ctx, 0, rule.ResourceId); err != nil {
|
||||
logger.L().Error("Failed to delete ACL resource", zap.Int("resourceId", rule.ResourceId), zap.Error(err))
|
||||
// Continue with database deletion even if ACL deletion fails
|
||||
}
|
||||
|
||||
// Delete the rule from database
|
||||
if err := tx.Delete(rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete V2 authorization rule: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -569,18 +643,543 @@ func (s *AuthorizationService) deleteV2AuthorizationRulesForAsset(ctx context.Co
|
||||
|
||||
// updateV2AuthorizationRulesForAsset updates V2 authorization rules for an asset
|
||||
func (s *AuthorizationService) updateV2AuthorizationRulesForAsset(ctx context.Context, tx *gorm.DB, asset *model.Asset, currentUser *acl.Session) error {
|
||||
// For simplicity, we'll delete existing rules and create new ones
|
||||
// This ensures consistency and handles complex permission changes
|
||||
|
||||
// First, delete existing rules for this asset
|
||||
if err := s.deleteV2AuthorizationRulesForAsset(ctx, tx, asset); err != nil {
|
||||
return fmt.Errorf("failed to delete existing V2 rules: %w", err)
|
||||
// Get current asset from database to compare with new data
|
||||
var currentAsset model.Asset
|
||||
if err := tx.Where("id = ?", asset.Id).First(¤tAsset).Error; err != nil {
|
||||
return fmt.Errorf("failed to get current asset: %w", err)
|
||||
}
|
||||
|
||||
// Then create new rules based on current asset.Authorization
|
||||
if err := s.createV2AuthorizationRulesForAsset(ctx, tx, asset, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to create new V2 rules: %w", err)
|
||||
// Create maps for efficient comparison
|
||||
currentAuthMap := make(map[int]*model.AccountAuthorization) // accountId -> current auth data
|
||||
for accountId, authData := range currentAsset.Authorization {
|
||||
currentAuthMap[accountId] = &authData
|
||||
}
|
||||
|
||||
newAuthMap := make(map[int]*model.AccountAuthorization) // accountId -> new auth data
|
||||
for accountId, authData := range asset.Authorization {
|
||||
authDataCopy := authData
|
||||
newAuthMap[accountId] = &authDataCopy
|
||||
}
|
||||
|
||||
// Process deletions: rules that exist in current but not in new
|
||||
for accountId, currentAuth := range currentAuthMap {
|
||||
if _, exists := newAuthMap[accountId]; !exists && currentAuth.RuleId > 0 {
|
||||
// Delete the specific V2 rule using RuleId
|
||||
if err := s.deleteV2AuthorizationRuleById(ctx, tx, currentAuth.RuleId); err != nil {
|
||||
return fmt.Errorf("failed to delete V2 rule %d for account %d: %w", currentAuth.RuleId, accountId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process creations and updates
|
||||
for accountId, newAuth := range newAuthMap {
|
||||
currentAuth, exists := currentAuthMap[accountId]
|
||||
|
||||
// Check if we need to create or find existing rule
|
||||
// This handles: 1) completely new authorization 2) historical data without RuleId
|
||||
if !exists || currentAuth.RuleId == 0 {
|
||||
// Before creating new rule, check if there's already an existing V2 rule
|
||||
// This handles historical data that doesn't have RuleId in asset.Authorization
|
||||
existingRule, err := s.findExistingV2Rule(tx, asset.Id, accountId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing V2 rule: %w", err)
|
||||
}
|
||||
|
||||
if existingRule != nil {
|
||||
// Found existing rule (either by JSON query or by name), update it
|
||||
if err := s.updateV2AuthorizationRuleById(ctx, tx, existingRule.Id, newAuth, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to update existing V2 rule %d for account %d: %w", existingRule.Id, accountId, err)
|
||||
}
|
||||
|
||||
// Set the found RuleId in the asset.Authorization field for future use
|
||||
newAuth.RuleId = existingRule.Id
|
||||
asset.Authorization[accountId] = *newAuth
|
||||
} else {
|
||||
// No existing rule found, create new one
|
||||
rule := &model.AuthorizationV2{
|
||||
Name: fmt.Sprintf("Asset-%d-Account-%d", asset.Id, accountId),
|
||||
Description: fmt.Sprintf("Auto-generated rule for asset %s and account %d", asset.Name, accountId),
|
||||
Enabled: true,
|
||||
|
||||
// Target selectors - specific asset and account
|
||||
AssetSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", asset.Id)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
AccountSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", accountId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
|
||||
// Use permissions from asset.Authorization
|
||||
Permissions: *newAuth.Permissions,
|
||||
|
||||
// Role IDs for ACL integration
|
||||
Rids: newAuth.Rids,
|
||||
|
||||
// Standard fields
|
||||
CreatorId: currentUser.GetUid(),
|
||||
UpdaterId: currentUser.GetUid(),
|
||||
}
|
||||
|
||||
// Create ACL resource for this rule
|
||||
resourceId, err := acl.CreateAcl(ctx, currentUser, config.RESOURCE_AUTHORIZATION, rule.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ACL resource: %w", err)
|
||||
}
|
||||
rule.ResourceId = resourceId
|
||||
|
||||
// Create the V2 rule
|
||||
if err := tx.Create(rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to create V2 authorization rule: %w", err)
|
||||
}
|
||||
|
||||
// Grant permissions to roles
|
||||
if len(newAuth.Rids) > 0 {
|
||||
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), newAuth.Rids, resourceId, []string{acl.READ}); err != nil {
|
||||
// Log error but continue - ACL resource was created successfully
|
||||
logger.L().Error("Failed to grant role permissions during rule creation",
|
||||
zap.Int("assetId", asset.Id),
|
||||
zap.Int("accountId", accountId),
|
||||
zap.Int("resourceId", resourceId),
|
||||
zap.Ints("rids", newAuth.Rids),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Update the asset.Authorization field with the new rule ID
|
||||
newAuth.RuleId = rule.Id
|
||||
asset.Authorization[accountId] = *newAuth
|
||||
}
|
||||
|
||||
} else {
|
||||
// Update existing rule using RuleId (normal case with complete data)
|
||||
if err := s.updateV2AuthorizationRuleById(ctx, tx, currentAuth.RuleId, newAuth, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to update V2 rule %d for account %d: %w", currentAuth.RuleId, accountId, err)
|
||||
}
|
||||
|
||||
// Keep the existing RuleId in the asset.Authorization field
|
||||
newAuth.RuleId = currentAuth.RuleId
|
||||
asset.Authorization[accountId] = *newAuth
|
||||
}
|
||||
}
|
||||
|
||||
// Update the asset's authorization field in database with updated rule IDs
|
||||
if err := tx.Model(asset).Update("authorization", asset.Authorization).Error; err != nil {
|
||||
return fmt.Errorf("failed to update asset authorization field: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findExistingV2Rule finds an existing V2 rule for the given asset and account combination
|
||||
func (s *AuthorizationService) findExistingV2Rule(tx *gorm.DB, assetId, accountId int) (*model.AuthorizationV2, error) {
|
||||
var rule model.AuthorizationV2
|
||||
|
||||
// First: Look for V2 rules that target this specific asset and account combination (JSON query)
|
||||
err := tx.Where(
|
||||
"asset_selector->>'$.type' = ? AND asset_selector->>'$.values' LIKE ? AND account_selector->>'$.type' = ? AND account_selector->>'$.values' LIKE ?",
|
||||
model.SelectorTypeIds,
|
||||
fmt.Sprintf("%%\"%d\"%%", assetId),
|
||||
model.SelectorTypeIds,
|
||||
fmt.Sprintf("%%\"%d\"%%", accountId),
|
||||
).First(&rule).Error
|
||||
|
||||
if err == nil {
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Second: Try to find by rule name pattern (for historical data compatibility)
|
||||
// Historical quick authorization rules follow the pattern: Asset-{assetId}-Account-{accountId}
|
||||
ruleName := fmt.Sprintf("Asset-%d-Account-%d", assetId, accountId)
|
||||
err = tx.Where("name = ?", ruleName).First(&rule).Error
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil // No existing rule found
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
// deleteV2AuthorizationRuleById deletes a specific V2 authorization rule by ID
|
||||
func (s *AuthorizationService) deleteV2AuthorizationRuleById(ctx context.Context, tx *gorm.DB, ruleId int) error {
|
||||
// Get the rule first to delete ACL resource
|
||||
var rule model.AuthorizationV2
|
||||
if err := tx.Where("id = ?", ruleId).First(&rule).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// Rule already deleted, no action needed
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to find V2 rule: %w", err)
|
||||
}
|
||||
|
||||
// Delete ACL resource
|
||||
if err := acl.DeleteResource(ctx, 0, rule.ResourceId); err != nil {
|
||||
logger.L().Error("Failed to delete ACL resource", zap.Int("resourceId", rule.ResourceId), zap.Error(err))
|
||||
// Continue with database deletion even if ACL deletion fails
|
||||
}
|
||||
|
||||
// Delete the rule from database
|
||||
if err := tx.Delete(&rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete V2 authorization rule: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateV2AuthorizationRuleById updates a specific V2 authorization rule by ID
|
||||
func (s *AuthorizationService) updateV2AuthorizationRuleById(ctx context.Context, tx *gorm.DB, ruleId int, newAuth *model.AccountAuthorization, currentUser *acl.Session) error {
|
||||
// Get the existing rule
|
||||
var existingRule model.AuthorizationV2
|
||||
if err := tx.Where("id = ?", ruleId).First(&existingRule).Error; err != nil {
|
||||
return fmt.Errorf("failed to find V2 rule: %w", err)
|
||||
}
|
||||
|
||||
needsUpdate := false
|
||||
|
||||
// Check if permissions changed
|
||||
if !reflect.DeepEqual(existingRule.Permissions, *newAuth.Permissions) {
|
||||
existingRule.Permissions = *newAuth.Permissions
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
// Check if role IDs changed
|
||||
if !reflect.DeepEqual(existingRule.Rids, newAuth.Rids) {
|
||||
// Revoke old role permissions
|
||||
if len(existingRule.Rids) > 0 {
|
||||
if err := acl.BatchRevokeRoleResource(ctx, currentUser.GetUid(), existingRule.Rids, existingRule.ResourceId, []string{acl.READ}); err != nil {
|
||||
// Log error but continue - ACL resource might not exist
|
||||
logger.L().Error("Failed to revoke old role permissions, continuing anyway",
|
||||
zap.Int("ruleId", ruleId),
|
||||
zap.Int("resourceId", existingRule.ResourceId),
|
||||
zap.Ints("rids", existingRule.Rids),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Grant new role permissions
|
||||
if len(newAuth.Rids) > 0 {
|
||||
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), newAuth.Rids, existingRule.ResourceId, []string{acl.READ}); err != nil {
|
||||
// Log error but continue - we can still update the database record
|
||||
logger.L().Error("Failed to grant new role permissions, continuing anyway",
|
||||
zap.Int("ruleId", ruleId),
|
||||
zap.Int("resourceId", existingRule.ResourceId),
|
||||
zap.Ints("rids", newAuth.Rids),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
existingRule.Rids = newAuth.Rids
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
// Update the rule if needed
|
||||
if needsUpdate {
|
||||
existingRule.UpdaterId = currentUser.GetUid()
|
||||
if err := tx.Model(&existingRule).Updates(map[string]interface{}{
|
||||
"permissions": existingRule.Permissions,
|
||||
"rids": existingRule.Rids,
|
||||
"updater_id": existingRule.UpdaterId,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("failed to update V2 authorization rule: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createV2AuthorizationRulesForNode creates V2 authorization rules for a node
|
||||
func (s *AuthorizationService) createV2AuthorizationRulesForNode(ctx context.Context, tx *gorm.DB, node *model.Node, currentUser *acl.Session) error {
|
||||
if len(node.Authorization) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for accountId, authData := range node.Authorization {
|
||||
// Check if there's already an existing V2 rule for this node-account combination
|
||||
// This handles cases where rules might exist from previous operations or historical data
|
||||
existingRule, err := s.findExistingV2RuleForNode(tx, node.Id, accountId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing V2 rule: %w", err)
|
||||
}
|
||||
|
||||
if existingRule != nil {
|
||||
// Found existing rule (either by JSON query or by name), update it instead of creating new one
|
||||
authDataCopy := authData
|
||||
if err := s.updateV2AuthorizationRuleById(ctx, tx, existingRule.Id, &authDataCopy, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to update existing V2 rule %d for account %d: %w", existingRule.Id, accountId, err)
|
||||
}
|
||||
|
||||
// Set the found RuleId in the node.Authorization field
|
||||
authDataCopy.RuleId = existingRule.Id
|
||||
node.Authorization[accountId] = authDataCopy
|
||||
|
||||
// Update the node's authorization field in database to include the rule ID
|
||||
if err := tx.Model(node).Update("authorization", node.Authorization).Error; err != nil {
|
||||
return fmt.Errorf("failed to update node authorization with existing rule ID: %w", err)
|
||||
}
|
||||
} else {
|
||||
// No existing rule found, create new one
|
||||
rule := &model.AuthorizationV2{
|
||||
Name: fmt.Sprintf("Node-%d-Account-%d", node.Id, accountId),
|
||||
Description: fmt.Sprintf("Auto-generated rule for node %s and account %d", node.Name, accountId),
|
||||
Enabled: true,
|
||||
|
||||
// Target selectors - specific node and account
|
||||
NodeSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", node.Id)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
AccountSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", accountId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
|
||||
// Use permissions from node.Authorization
|
||||
Permissions: *authData.Permissions,
|
||||
|
||||
// Role IDs for ACL integration
|
||||
Rids: authData.Rids,
|
||||
|
||||
// Standard fields
|
||||
CreatorId: currentUser.GetUid(),
|
||||
UpdaterId: currentUser.GetUid(),
|
||||
}
|
||||
|
||||
// Create ACL resource for this rule
|
||||
resourceId, err := acl.CreateAcl(ctx, currentUser, config.RESOURCE_AUTHORIZATION, rule.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ACL resource: %w", err)
|
||||
}
|
||||
rule.ResourceId = resourceId
|
||||
|
||||
// Create the V2 rule
|
||||
if err := tx.Create(rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to create V2 authorization rule: %w", err)
|
||||
}
|
||||
|
||||
// Grant permissions to roles
|
||||
if len(authData.Rids) > 0 {
|
||||
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), authData.Rids, resourceId, []string{acl.READ}); err != nil {
|
||||
// Log error but continue - ACL resource was created successfully
|
||||
logger.L().Error("Failed to grant role permissions during rule creation",
|
||||
zap.Int("nodeId", node.Id),
|
||||
zap.Int("accountId", accountId),
|
||||
zap.Int("resourceId", resourceId),
|
||||
zap.Ints("rids", authData.Rids),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Important: Update the node.Authorization field with the rule ID
|
||||
authDataCopy := authData
|
||||
authDataCopy.RuleId = rule.Id
|
||||
node.Authorization[accountId] = authDataCopy
|
||||
|
||||
// Update the node's authorization field in database to include the rule ID
|
||||
if err := tx.Model(node).Update("authorization", node.Authorization).Error; err != nil {
|
||||
return fmt.Errorf("failed to update node authorization with rule ID: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteV2AuthorizationRulesForNode deletes V2 authorization rules for a node
|
||||
func (s *AuthorizationService) deleteV2AuthorizationRulesForNode(ctx context.Context, tx *gorm.DB, node *model.Node) error {
|
||||
// Delete rules based on RuleId stored in node.Authorization field
|
||||
for accountId, authData := range node.Authorization {
|
||||
if authData.RuleId > 0 {
|
||||
if err := s.deleteV2AuthorizationRuleById(ctx, tx, authData.RuleId); err != nil {
|
||||
// Log error but continue with other deletions
|
||||
logger.L().Error("Failed to delete V2 rule for node",
|
||||
zap.Int("nodeId", node.Id),
|
||||
zap.Int("accountId", accountId),
|
||||
zap.Int("ruleId", authData.RuleId),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateV2AuthorizationRulesForNode updates V2 authorization rules for a node
|
||||
func (s *AuthorizationService) updateV2AuthorizationRulesForNode(ctx context.Context, tx *gorm.DB, node *model.Node, currentUser *acl.Session) error {
|
||||
// Get current node from database to compare with new data
|
||||
var currentNode model.Node
|
||||
if err := tx.Where("id = ?", node.Id).First(¤tNode).Error; err != nil {
|
||||
return fmt.Errorf("failed to get current node: %w", err)
|
||||
}
|
||||
|
||||
// Create maps for efficient comparison
|
||||
currentAuthMap := make(map[int]*model.AccountAuthorization) // accountId -> current auth data
|
||||
for accountId, authData := range currentNode.Authorization {
|
||||
currentAuthMap[accountId] = &authData
|
||||
}
|
||||
|
||||
newAuthMap := make(map[int]*model.AccountAuthorization) // accountId -> new auth data
|
||||
for accountId, authData := range node.Authorization {
|
||||
authDataCopy := authData
|
||||
newAuthMap[accountId] = &authDataCopy
|
||||
}
|
||||
|
||||
// Process deletions: rules that exist in current but not in new
|
||||
for accountId, currentAuth := range currentAuthMap {
|
||||
if _, exists := newAuthMap[accountId]; !exists && currentAuth.RuleId > 0 {
|
||||
// Delete the specific V2 rule using RuleId
|
||||
if err := s.deleteV2AuthorizationRuleById(ctx, tx, currentAuth.RuleId); err != nil {
|
||||
return fmt.Errorf("failed to delete V2 rule %d for account %d: %w", currentAuth.RuleId, accountId, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process creations and updates
|
||||
for accountId, newAuth := range newAuthMap {
|
||||
currentAuth, exists := currentAuthMap[accountId]
|
||||
|
||||
// Check if we need to create or find existing rule
|
||||
// This handles: 1) completely new authorization 2) historical data without RuleId
|
||||
if !exists || currentAuth.RuleId == 0 {
|
||||
// Before creating new rule, check if there's already an existing V2 rule
|
||||
// This handles historical data that doesn't have RuleId in node.Authorization
|
||||
existingRule, err := s.findExistingV2RuleForNode(tx, node.Id, accountId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing V2 rule: %w", err)
|
||||
}
|
||||
|
||||
if existingRule != nil {
|
||||
// Found existing rule (either by JSON query or by name), update it
|
||||
if err := s.updateV2AuthorizationRuleById(ctx, tx, existingRule.Id, newAuth, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to update existing V2 rule %d for account %d: %w", existingRule.Id, accountId, err)
|
||||
}
|
||||
|
||||
// Set the found RuleId in the node.Authorization field for future use
|
||||
newAuth.RuleId = existingRule.Id
|
||||
node.Authorization[accountId] = *newAuth
|
||||
} else {
|
||||
// No existing rule found, create new one
|
||||
rule := &model.AuthorizationV2{
|
||||
Name: fmt.Sprintf("Node-%d-Account-%d", node.Id, accountId),
|
||||
Description: fmt.Sprintf("Auto-generated rule for node %s and account %d", node.Name, accountId),
|
||||
Enabled: true,
|
||||
|
||||
// Target selectors - specific node and account
|
||||
NodeSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", node.Id)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
AccountSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", accountId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
|
||||
// Use permissions from node.Authorization
|
||||
Permissions: *newAuth.Permissions,
|
||||
|
||||
// Role IDs for ACL integration
|
||||
Rids: newAuth.Rids,
|
||||
|
||||
// Standard fields
|
||||
CreatorId: currentUser.GetUid(),
|
||||
UpdaterId: currentUser.GetUid(),
|
||||
}
|
||||
|
||||
// Create ACL resource for this rule
|
||||
resourceId, err := acl.CreateAcl(ctx, currentUser, config.RESOURCE_AUTHORIZATION, rule.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ACL resource: %w", err)
|
||||
}
|
||||
rule.ResourceId = resourceId
|
||||
|
||||
// Create the V2 rule
|
||||
if err := tx.Create(rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to create V2 authorization rule: %w", err)
|
||||
}
|
||||
|
||||
// Grant permissions to roles
|
||||
if len(newAuth.Rids) > 0 {
|
||||
if err := acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), newAuth.Rids, resourceId, []string{acl.READ}); err != nil {
|
||||
// Log error but continue - ACL resource was created successfully
|
||||
logger.L().Error("Failed to grant role permissions during rule creation",
|
||||
zap.Int("nodeId", node.Id),
|
||||
zap.Int("accountId", accountId),
|
||||
zap.Int("resourceId", resourceId),
|
||||
zap.Ints("rids", newAuth.Rids),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Update the node.Authorization field with the new rule ID
|
||||
newAuth.RuleId = rule.Id
|
||||
node.Authorization[accountId] = *newAuth
|
||||
}
|
||||
|
||||
} else {
|
||||
// Update existing rule using RuleId (normal case with complete data)
|
||||
if err := s.updateV2AuthorizationRuleById(ctx, tx, currentAuth.RuleId, newAuth, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to update V2 rule %d for account %d: %w", currentAuth.RuleId, accountId, err)
|
||||
}
|
||||
|
||||
// Keep the existing RuleId in the node.Authorization field
|
||||
newAuth.RuleId = currentAuth.RuleId
|
||||
node.Authorization[accountId] = *newAuth
|
||||
}
|
||||
}
|
||||
|
||||
// Update the node's authorization field in database with updated rule IDs
|
||||
if err := tx.Model(node).Update("authorization", node.Authorization).Error; err != nil {
|
||||
return fmt.Errorf("failed to update node authorization field: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findExistingV2RuleForNode finds an existing V2 rule for the given node and account combination
|
||||
func (s *AuthorizationService) findExistingV2RuleForNode(tx *gorm.DB, nodeId, accountId int) (*model.AuthorizationV2, error) {
|
||||
var rule model.AuthorizationV2
|
||||
|
||||
// First: Look for V2 rules that target this specific node and account combination (JSON query)
|
||||
err := tx.Where(
|
||||
"node_selector->>'$.type' = ? AND node_selector->>'$.values' LIKE ? AND account_selector->>'$.type' = ? AND account_selector->>'$.values' LIKE ?",
|
||||
model.SelectorTypeIds,
|
||||
fmt.Sprintf("%%\"%d\"%%", nodeId),
|
||||
model.SelectorTypeIds,
|
||||
fmt.Sprintf("%%\"%d\"%%", accountId),
|
||||
).First(&rule).Error
|
||||
|
||||
if err == nil {
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Second: Try to find by rule name pattern (for historical data compatibility)
|
||||
// Historical quick authorization rules follow the pattern: Node-{nodeId}-Account-{accountId}
|
||||
ruleName := fmt.Sprintf("Node-%d-Account-%d", nodeId, accountId)
|
||||
err = tx.Where("name = ?", ruleName).First(&rule).Error
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil // No existing rule found
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &rule, nil
|
||||
}
|
||||
|
@@ -483,27 +483,73 @@ func migrateAssetAuthorization(ctx context.Context, assetId int) error {
|
||||
// Get default permissions from config
|
||||
defaultPermissions := getDefaultAuthPermissions()
|
||||
|
||||
// Convert V1 to V2 format
|
||||
// Start transaction for atomic operation
|
||||
return dbpkg.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// Convert V1 to V2 format AND create V2 authorization rules
|
||||
v2Auth := make(map[int]model.AccountAuthorization)
|
||||
for accountId, roleIds := range v1Auth {
|
||||
// Create V2 authorization rule for this asset-account combination
|
||||
rule := &model.AuthorizationV2{
|
||||
Name: fmt.Sprintf("Asset-%d-Account-%d-Migrated", assetId, accountId),
|
||||
Description: fmt.Sprintf("Migrated rule for asset %d and account %d", assetId, accountId),
|
||||
Enabled: true,
|
||||
|
||||
// Target selectors - specific asset and account
|
||||
AssetSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", assetId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
AccountSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", accountId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
|
||||
// Use default permissions
|
||||
Permissions: defaultPermissions,
|
||||
|
||||
// Role IDs from V1 data
|
||||
Rids: roleIds,
|
||||
|
||||
// Standard fields (use system user for migration)
|
||||
CreatorId: 1, // System user
|
||||
UpdaterId: 1, // System user
|
||||
}
|
||||
|
||||
// Create ACL resource for this rule
|
||||
resourceId, err := createBasicACLResourceForMigration(rule.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ACL resource: %w", err)
|
||||
}
|
||||
rule.ResourceId = resourceId
|
||||
|
||||
// Create the V2 rule
|
||||
if err := tx.Create(rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to create V2 authorization rule: %w", err)
|
||||
}
|
||||
|
||||
// Convert to V2 format with RuleId
|
||||
v2Auth[accountId] = model.AccountAuthorization{
|
||||
Rids: roleIds,
|
||||
Permissions: &defaultPermissions,
|
||||
RuleId: rule.Id, // Set the rule ID for tracking
|
||||
}
|
||||
}
|
||||
|
||||
// Update the database
|
||||
if err := dbpkg.DB.Model(&model.Asset{}).
|
||||
// Update the database with V2 format including RuleIds
|
||||
if err := tx.Model(&model.Asset{}).
|
||||
Where("id = ?", assetId).
|
||||
Update("authorization", v2Auth).Error; err != nil {
|
||||
return fmt.Errorf("failed to update authorization data: %w", err)
|
||||
}
|
||||
|
||||
logger.L().Debug("Migrated asset authorization data",
|
||||
logger.L().Debug("Migrated asset authorization data with V2 rules",
|
||||
zap.Int("assetId", assetId),
|
||||
zap.Int("accounts", len(v1Auth)))
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// getDefaultAuthPermissions returns default permissions for migration
|
||||
@@ -583,29 +629,94 @@ func (s *AuthorizationMigrationService) migrateAssetAuthorizationField(ctx conte
|
||||
// Get default permissions
|
||||
defaultPermissions := s.getDefaultAuthPermissions()
|
||||
|
||||
// Convert V1 to V2 format
|
||||
// Convert V1 to V2 format AND create V2 authorization rules
|
||||
v2Auth := make(map[int]model.AccountAuthorization)
|
||||
for accountId, roleIds := range v1Auth {
|
||||
// Create V2 authorization rule for this asset-account combination
|
||||
rule := &model.AuthorizationV2{
|
||||
Name: fmt.Sprintf("Asset-%d-Account-%d-Migrated", assetId, accountId),
|
||||
Description: fmt.Sprintf("Migrated rule for asset %d and account %d", assetId, accountId),
|
||||
Enabled: true,
|
||||
|
||||
// Target selectors - specific asset and account
|
||||
AssetSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", assetId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
AccountSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", accountId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
|
||||
// Use default permissions
|
||||
Permissions: defaultPermissions,
|
||||
|
||||
// Role IDs from V1 data
|
||||
Rids: roleIds,
|
||||
|
||||
// Standard fields (use system user for migration)
|
||||
CreatorId: 1, // System user
|
||||
UpdaterId: 1, // System user
|
||||
}
|
||||
|
||||
// Create ACL resource for this rule
|
||||
// Note: For migration, we'll create a basic ACL resource without user context
|
||||
resourceId, err := s.createBasicACLResource(rule.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create ACL resource: %w", err)
|
||||
}
|
||||
rule.ResourceId = resourceId
|
||||
|
||||
// Create the V2 rule
|
||||
if err := tx.Create(rule).Error; err != nil {
|
||||
return fmt.Errorf("failed to create V2 authorization rule: %w", err)
|
||||
}
|
||||
|
||||
// Convert to V2 format with RuleId
|
||||
v2Auth[accountId] = model.AccountAuthorization{
|
||||
Rids: roleIds,
|
||||
Permissions: &defaultPermissions,
|
||||
RuleId: rule.Id, // Set the rule ID for tracking
|
||||
}
|
||||
}
|
||||
|
||||
// Update the database
|
||||
// Update the database with V2 format including RuleIds
|
||||
if err := tx.Model(&model.Asset{}).
|
||||
Where("id = ?", assetId).
|
||||
Update("authorization", v2Auth).Error; err != nil {
|
||||
return fmt.Errorf("failed to update authorization data: %w", err)
|
||||
}
|
||||
|
||||
logger.L().Debug("Migrated asset authorization data",
|
||||
logger.L().Debug("Migrated asset authorization data with V2 rules",
|
||||
zap.Int("assetId", assetId),
|
||||
zap.Int("accounts", len(v1Auth)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createBasicACLResource creates a basic ACL resource for migration purposes
|
||||
func (s *AuthorizationMigrationService) createBasicACLResource(name string) (int, error) {
|
||||
// For migration, we'll create a simplified ACL resource
|
||||
// This is a basic implementation - in a real system, you might need to
|
||||
// integrate with the actual ACL system more thoroughly
|
||||
|
||||
// Note: This is a placeholder implementation
|
||||
// In the actual system, you would need to use the proper ACL creation methods
|
||||
// For now, we'll create a unique resource ID (this should be replaced with actual ACL integration)
|
||||
|
||||
// Generate a simple resource ID based on current time
|
||||
// In real implementation, this should use the actual ACL system
|
||||
resourceId := int(time.Now().UnixNano() % 1000000)
|
||||
|
||||
logger.L().Debug("Created basic ACL resource for migration",
|
||||
zap.String("name", name),
|
||||
zap.Int("resourceId", resourceId))
|
||||
|
||||
return resourceId, nil
|
||||
}
|
||||
|
||||
// getDefaultAuthPermissions returns default permissions for migration service
|
||||
func (s *AuthorizationMigrationService) getDefaultAuthPermissions() model.AuthPermissions {
|
||||
// Get from config if available
|
||||
@@ -623,3 +734,158 @@ func (s *AuthorizationMigrationService) getDefaultAuthPermissions() model.AuthPe
|
||||
Share: false,
|
||||
}
|
||||
}
|
||||
|
||||
// createBasicACLResourceForMigration creates a basic ACL resource for migration purposes (standalone function)
|
||||
func createBasicACLResourceForMigration(name string) (int, error) {
|
||||
// For migration, we'll create a simplified ACL resource
|
||||
// This is a basic implementation - in a real system, you might need to
|
||||
// integrate with the actual ACL system more thoroughly
|
||||
|
||||
// Generate a simple resource ID based on current time
|
||||
// In real implementation, this should use the actual ACL system
|
||||
resourceId := int(time.Now().UnixNano() % 1000000)
|
||||
|
||||
logger.L().Debug("Created basic ACL resource for migration",
|
||||
zap.String("name", name),
|
||||
zap.Int("resourceId", resourceId))
|
||||
|
||||
return resourceId, nil
|
||||
}
|
||||
|
||||
// MigrateNodeAuthorization migrates node authorization from V1 to V2 format
|
||||
func MigrateNodeAuthorization() error {
|
||||
logger.L().Info("Starting node authorization V1 to V2 migration")
|
||||
|
||||
// Get all nodes that need migration
|
||||
var nodes []*model.Node
|
||||
if err := dbpkg.DB.Find(&nodes).Error; err != nil {
|
||||
return fmt.Errorf("failed to fetch nodes: %w", err)
|
||||
}
|
||||
|
||||
logger.L().Info("Found nodes for migration", zap.Int("count", len(nodes)))
|
||||
|
||||
migratedCount := 0
|
||||
for _, node := range nodes {
|
||||
if len(node.Authorization) == 0 {
|
||||
continue // Skip nodes without authorization
|
||||
}
|
||||
|
||||
// Check if node needs migration (has V1 format)
|
||||
needsMigration := false
|
||||
for _, authData := range node.Authorization {
|
||||
if authData.Permissions == nil {
|
||||
needsMigration = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !needsMigration {
|
||||
continue // Skip nodes that are already in V2 format
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := migrateNodeAuthorizationField(ctx, dbpkg.DB, node.Id); err != nil {
|
||||
logger.L().Error("Failed to migrate node authorization",
|
||||
zap.Int("nodeId", node.Id),
|
||||
zap.String("nodeName", node.Name),
|
||||
zap.Error(err))
|
||||
continue // Continue with other nodes
|
||||
}
|
||||
|
||||
migratedCount++
|
||||
logger.L().Info("Successfully migrated node authorization",
|
||||
zap.Int("nodeId", node.Id),
|
||||
zap.String("nodeName", node.Name))
|
||||
}
|
||||
|
||||
logger.L().Info("Node authorization migration completed",
|
||||
zap.Int("migratedCount", migratedCount),
|
||||
zap.Int("totalNodes", len(nodes)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateNodeAuthorizationField migrates a specific node's authorization field from V1 to V2
|
||||
func migrateNodeAuthorizationField(ctx context.Context, tx *gorm.DB, nodeId int) error {
|
||||
// Get current node data
|
||||
var node model.Node
|
||||
if err := tx.Where("id = ?", nodeId).First(&node).Error; err != nil {
|
||||
return fmt.Errorf("failed to get node: %w", err)
|
||||
}
|
||||
|
||||
if len(node.Authorization) == 0 {
|
||||
return nil // No authorization to migrate
|
||||
}
|
||||
|
||||
// Convert V1 authorization to V2 format and create actual V2 rules
|
||||
defaultPermissions := getDefaultAuthPermissions()
|
||||
|
||||
for accountId, authData := range node.Authorization {
|
||||
// Skip if already V2 format
|
||||
if authData.Permissions != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create V2 authorization rule
|
||||
rule := &model.AuthorizationV2{
|
||||
Name: fmt.Sprintf("Node-%d-Account-%d", nodeId, accountId),
|
||||
Description: fmt.Sprintf("Migrated rule for node %s and account %d", node.Name, accountId),
|
||||
Enabled: true,
|
||||
|
||||
// Target selectors - specific node and account
|
||||
NodeSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", nodeId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
AccountSelector: model.TargetSelector{
|
||||
Type: model.SelectorTypeIds,
|
||||
Values: []string{fmt.Sprintf("%d", accountId)},
|
||||
ExcludeIds: []int{},
|
||||
},
|
||||
|
||||
// Use default permissions for migration
|
||||
Permissions: defaultPermissions,
|
||||
|
||||
// Use existing role IDs
|
||||
Rids: authData.Rids,
|
||||
|
||||
// Standard fields
|
||||
CreatorId: 1, // System migration
|
||||
UpdaterId: 1, // System migration
|
||||
}
|
||||
|
||||
// Create placeholder ACL resource for migration
|
||||
resourceId, err := createBasicACLResourceForMigration(rule.Name)
|
||||
if err != nil {
|
||||
logger.L().Error("Failed to create ACL resource during migration",
|
||||
zap.String("ruleName", rule.Name),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
rule.ResourceId = resourceId
|
||||
|
||||
// Create the V2 rule
|
||||
if err := tx.Create(rule).Error; err != nil {
|
||||
logger.L().Error("Failed to create V2 authorization rule during migration",
|
||||
zap.String("ruleName", rule.Name),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the node.Authorization field with V2 format including RuleId
|
||||
v2AuthData := model.AccountAuthorization{
|
||||
Rids: authData.Rids,
|
||||
Permissions: &defaultPermissions,
|
||||
RuleId: rule.Id, // Important: Link to the created rule
|
||||
}
|
||||
node.Authorization[accountId] = v2AuthData
|
||||
}
|
||||
|
||||
// Update the node's authorization field in database
|
||||
if err := tx.Model(&node).Update("authorization", node.Authorization).Error; err != nil {
|
||||
return fmt.Errorf("failed to update node authorization: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user