mirror of
https://github.com/juanfont/headscale.git
synced 2026-01-23 02:24:10 +00:00
added validations before migrating tags
This commit is contained in:
parent
9f99c4fd36
commit
740f650a4d
1 changed files with 101 additions and 11 deletions
|
|
@ -10,6 +10,7 @@ import (
|
|||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
|
|
@ -594,28 +595,88 @@ AND auth_key_id NOT IN (
|
|||
},
|
||||
{
|
||||
// Migrate RequestTags from host_info JSON to tags column.
|
||||
// In 0.27.x, tags from --advertise-tags (ValidTags) were stored only in
|
||||
// host_info.RequestTags, not in the tags column (formerly forced_tags).
|
||||
// This migration validates RequestTags against the policy's tagOwners
|
||||
// and merges validated tags into the tags column.
|
||||
// Fixes: https://github.com/juanfont/headscale/issues/3006
|
||||
ID: "202601121700-migrate-hostinfo-request-tags",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// Define a minimal struct to read node data
|
||||
// 1. Load policy from database
|
||||
var policyData string
|
||||
err := tx.Raw("SELECT data FROM policies ORDER BY id DESC LIMIT 1").Scan(&policyData).Error
|
||||
if err != nil || policyData == "" {
|
||||
log.Info().Msg("No policy found in database, skipping RequestTags migration (tags will be validated on node reconnect)")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. Parse tagOwners and groups from policy
|
||||
type migrationPolicy struct {
|
||||
TagOwners map[string][]string `json:"tagOwners"`
|
||||
Groups map[string][]string `json:"groups"`
|
||||
}
|
||||
var pol migrationPolicy
|
||||
if err := json.Unmarshal([]byte(policyData), &pol); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to parse policy JSON, skipping RequestTags migration (tags will be validated on node reconnect)")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(pol.TagOwners) == 0 {
|
||||
log.Info().Msg("No tagOwners defined in policy, skipping RequestTags migration")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to check if a user can have a tag
|
||||
canUserHaveTag := func(username string, tag string) bool {
|
||||
owners, exists := pol.TagOwners[tag]
|
||||
if !exists {
|
||||
return false // Tag not defined in policy
|
||||
}
|
||||
|
||||
for _, owner := range owners {
|
||||
// Direct username match
|
||||
if owner == username {
|
||||
return true
|
||||
}
|
||||
|
||||
// Group expansion
|
||||
if strings.HasPrefix(owner, "group:") {
|
||||
if groupMembers, ok := pol.Groups[owner]; ok {
|
||||
if slices.Contains(groupMembers, username) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. Query nodes with user info
|
||||
type nodeRow struct {
|
||||
ID uint64
|
||||
HostInfo string
|
||||
Tags string
|
||||
UserID *uint64
|
||||
Username *string
|
||||
}
|
||||
|
||||
var nodes []nodeRow
|
||||
err := tx.Raw("SELECT id, host_info, tags FROM nodes WHERE host_info IS NOT NULL AND host_info != '' AND host_info != '{}'").Scan(&nodes).Error
|
||||
err = tx.Raw(`
|
||||
SELECT n.id, n.host_info, n.tags, n.user_id, u.name as username
|
||||
FROM nodes n
|
||||
LEFT JOIN users u ON n.user_id = u.id
|
||||
WHERE n.host_info IS NOT NULL AND n.host_info != '' AND n.host_info != '{}'
|
||||
`).Scan(&nodes).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("querying nodes for RequestTags migration: %w", err)
|
||||
}
|
||||
|
||||
// 4. Process each node
|
||||
for _, node := range nodes {
|
||||
// Parse host_info JSON to extract RequestTags
|
||||
var hostInfo struct {
|
||||
RequestTags []string `json:"RequestTags"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(node.HostInfo), &hostInfo); err != nil {
|
||||
// Skip nodes with invalid JSON - they may have been cleaned up
|
||||
log.Trace().
|
||||
Uint64("node.id", node.ID).
|
||||
Err(err).
|
||||
|
|
@ -628,6 +689,15 @@ AND auth_key_id NOT IN (
|
|||
continue
|
||||
}
|
||||
|
||||
// Skip if no username (can't validate)
|
||||
if node.Username == nil || *node.Username == "" {
|
||||
log.Debug().
|
||||
Uint64("node.id", node.ID).
|
||||
Strs("request_tags", hostInfo.RequestTags).
|
||||
Msg("Skipping node without username during RequestTags migration")
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse existing tags from the tags column
|
||||
var existingTags []string
|
||||
if node.Tags != "" && node.Tags != "null" {
|
||||
|
|
@ -640,15 +710,33 @@ AND auth_key_id NOT IN (
|
|||
}
|
||||
}
|
||||
|
||||
// Merge RequestTags with existing tags
|
||||
mergedTags := existingTags
|
||||
// Validate and merge RequestTags
|
||||
var validatedTags []string
|
||||
var rejectedTags []string
|
||||
for _, tag := range hostInfo.RequestTags {
|
||||
if !slices.Contains(mergedTags, tag) {
|
||||
mergedTags = append(mergedTags, tag)
|
||||
if canUserHaveTag(*node.Username, tag) {
|
||||
if !slices.Contains(existingTags, tag) {
|
||||
validatedTags = append(validatedTags, tag)
|
||||
}
|
||||
} else {
|
||||
rejectedTags = append(rejectedTags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort and compact
|
||||
// Skip if no validated tags to add
|
||||
if len(validatedTags) == 0 {
|
||||
if len(rejectedTags) > 0 {
|
||||
log.Debug().
|
||||
Uint64("node.id", node.ID).
|
||||
Str("username", *node.Username).
|
||||
Strs("rejected_tags", rejectedTags).
|
||||
Msg("RequestTags rejected during migration (user not authorized)")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Merge validated tags with existing tags
|
||||
mergedTags := append(existingTags, validatedTags...)
|
||||
slices.Sort(mergedTags)
|
||||
mergedTags = slices.Compact(mergedTags)
|
||||
|
||||
|
|
@ -665,10 +753,12 @@ AND auth_key_id NOT IN (
|
|||
|
||||
log.Info().
|
||||
Uint64("node.id", node.ID).
|
||||
Strs("request_tags", hostInfo.RequestTags).
|
||||
Str("username", *node.Username).
|
||||
Strs("validated_tags", validatedTags).
|
||||
Strs("rejected_tags", rejectedTags).
|
||||
Strs("existing_tags", existingTags).
|
||||
Strs("merged_tags", mergedTags).
|
||||
Msg("Migrated RequestTags from host_info to tags column")
|
||||
Msg("Migrated validated RequestTags from host_info to tags column")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue