state: auto-approve routes after an admin tag change

The new tags may approve announced routes. Approval runs once the policy
manager knows the new owner, as after a registration.
This commit is contained in:
Kristoffer Dalby committed 2026-10-30 14:55:36 +00:00
1 parent cd09ccaf34
commit 9127edbd6d
3 files changed
+38 -8

No files matched your search

@@ -39,10 +39,6 @@ import (
var reauthSkipReasons = map[string]string{
"owner-routes/r-expired-retag/expire/online": expiredPeerOffline,
"owner-routes/r-admin/owner/enabled": adminRetagNoApproval,
"owner-routes/r-admin/owner/served": adminRetagNoApproval,
"owner-routes/r-admin/owner/self-routes": adminRetagNoApproval,
"identity/w-tagreq-relogin-tags/w-tagreq/owner": keyLoginTagsRefused,
"identity/w-tagreq-relogin-tags/w-tagreq/outcome": keyLoginTagsRefused,
"identity/w-tagreq-relogin-tags/w-tagreq/node-key": keyLoginTagsRefused,
@@ -81,10 +77,6 @@ var reauthSkipReasons = map[string]string{
"ephemeral-lifecycle/late-expired/late-exp/legacy-text": refusalText,
}
const (
adminRetagNoApproval = "ADMIN_RETAG_NO_APPROVAL: an admin tag change does not auto-approve the routes the new tags may approve; SaaS approves them at once"
)
const (
keyLoginTagsRefused = "KEY_LOGIN_TAGS_REFUSED: headscale refuses a user's key login that requests tags the user owns; SaaS tags the device, converting it in place or creating it tagged"
taggedKeyOwnedTagRefused = "TAGGED_KEY_OWNED_TAG_REFUSED: headscale refuses a tagged key's login that requests a tag the key's tags own; SaaS gives the device the requested tags"
+24
View File
@@ -349,3 +349,27 @@ func TestRegistrationDropsWithdrawnAutoApprovals(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, []netip.Prefix{kept}, approvedRoutes(t, s, node.ID()))
}
// TestAdminTagChangeApprovesRoutes: an admin tag change runs route
// auto-approval for the routes the new tags may approve.
func TestAdminTagChangeApprovesRoutes(t *testing.T) {
_, s, id := persistTestSetup(t)
t.Cleanup(func() { _ = s.Close() })
_, err := s.SetPolicy([]byte(`{
"tagOwners": {"tag:router": ["persist-user@"]},
"autoApprovers": {"routes": {"10.0.0.0/16": ["tag:router"]}},
"acls": [{"action": "accept", "src": ["*"], "dst": ["*:*"]}]
}`))
require.NoError(t, err)
route := netip.MustParsePrefix("10.0.1.0/24")
announce(t, s, id, route)
require.Empty(t, approvedRoutes(t, s, id), "the user may not approve it")
node, _, err := s.SetNodeTags(id, []string{"tag:router"})
require.NoError(t, err)
assert.Equal(t, []netip.Prefix{route}, node.ApprovedRoutes().AsSlice())
assert.Equal(t, []netip.Prefix{route}, approvedRoutes(t, s, id))
}
+14
View File
@@ -1264,6 +1264,20 @@ func (s *State) SetNodeTags(nodeID types.NodeID, tags []string) (types.NodeView,
// Setting OriginNode ensures the node gets a self-update with the new tags.
c.OriginNode = nodeID
// The new tags may approve announced routes, as on Tailscale SaaS. This
// runs after the policy refresh above, which teaches the policy manager
// the node's new owner.
routesChange, err := s.AutoApproveRoutes(nodeView)
c = c.Merge(routesChange)
if fresh, ok := s.nodeStore.GetNode(nodeID); ok {
nodeView = fresh
}
if err != nil {
return nodeView, c, fmt.Errorf("auto approving routes: %w", err)
}
return nodeView, c, nil
}