db: use PolicyManager for RequestTags migration

Refactor the RequestTags migration (202601121700-migrate-hostinfo-request-tags)
to use PolicyManager.NodeCanHaveTag() instead of reimplementing tag validation.

Changes:
- NewHeadscaleDatabase now accepts *types.Config to allow migrations
  access to policy configuration
- Add loadPolicyBytes helper to load policy from file or DB based on config
- Add standalone GetPolicy(tx *gorm.DB) for use during migrations
- Replace custom tag validation logic with PolicyManager

Benefits:
- Full HuJSON parsing support (not just JSON)
- Proper group expansion via PolicyManager
- Support for nested tags and autogroups
- Works with both file and database policy modes
- Single source of truth for tag validation


Co-Authored-By: Shourya Gautam <shouryamgautam@gmail.com>
This commit is contained in:
Shourya Gautam
2026-01-21 19:40:29 +05:30
committed by GitHub
parent 22afb2c61b
commit 4e1834adaf
9 changed files with 413 additions and 103 deletions

View File

@@ -5,6 +5,7 @@ import (
"strings"
"time"
hsdb "github.com/juanfont/headscale/hscontrol/db"
"github.com/juanfont/headscale/hscontrol/routes"
"github.com/juanfont/headscale/hscontrol/types"
"tailscale.com/tailcfg"
@@ -228,7 +229,7 @@ func (s *State) DebugPolicy() (string, error) {
return p.Data, nil
case types.PolicyModeFile:
pol, err := policyBytes(s.db, s.cfg)
pol, err := hsdb.PolicyBytes(s.db.DB, s.cfg)
if err != nil {
return "", err
}

View File

@@ -8,9 +8,7 @@ import (
"context"
"errors"
"fmt"
"io"
"net/netip"
"os"
"slices"
"strings"
"sync"
@@ -115,8 +113,7 @@ func NewState(cfg *types.Config) (*State, error) {
)
db, err := hsdb.NewHeadscaleDatabase(
cfg.Database,
cfg.BaseDomain,
cfg,
registrationCache,
)
if err != nil {
@@ -143,7 +140,7 @@ func NewState(cfg *types.Config) (*State, error) {
return nil, fmt.Errorf("loading users: %w", err)
}
pol, err := policyBytes(db, cfg)
pol, err := hsdb.PolicyBytes(db.DB, cfg)
if err != nil {
return nil, fmt.Errorf("loading policy: %w", err)
}
@@ -199,47 +196,6 @@ func (s *State) Close() error {
return nil
}
// policyBytes loads policy configuration from file or database based on the configured mode.
// Returns nil if no policy is configured, which is valid.
func policyBytes(db *hsdb.HSDatabase, cfg *types.Config) ([]byte, error) {
switch cfg.Policy.Mode {
case types.PolicyModeFile:
path := cfg.Policy.Path
// It is fine to start headscale without a policy file.
if len(path) == 0 {
return nil, nil
}
absPath := util.AbsolutePathFromConfigPath(path)
policyFile, err := os.Open(absPath)
if err != nil {
return nil, err
}
defer policyFile.Close()
return io.ReadAll(policyFile)
case types.PolicyModeDB:
p, err := db.GetPolicy()
if err != nil {
if errors.Is(err, types.ErrPolicyNotFound) {
return nil, nil
}
return nil, err
}
if p.Data == "" {
return nil, nil
}
return []byte(p.Data), err
}
return nil, fmt.Errorf("%w: %s", ErrUnsupportedPolicyMode, cfg.Policy.Mode)
}
// SetDERPMap updates the DERP relay configuration.
func (s *State) SetDERPMap(dm *tailcfg.DERPMap) {
s.derpMap.Store(dm)
@@ -253,7 +209,7 @@ func (s *State) DERPMap() tailcfg.DERPMapView {
// ReloadPolicy reloads the access control policy and triggers auto-approval if changed.
// Returns true if the policy changed.
func (s *State) ReloadPolicy() ([]change.Change, error) {
pol, err := policyBytes(s.db, s.cfg)
pol, err := hsdb.PolicyBytes(s.db.DB, s.cfg)
if err != nil {
return nil, fmt.Errorf("loading policy: %w", err)
}