Skip to content

Commit a12f295

Browse files
committed
policy: move flattenTags and flattenTagOwners to policy.go
1 parent 5323366 commit a12f295

File tree

3 files changed

+100
-18
lines changed

3 files changed

+100
-18
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ backwards compatibility.
2525

2626
Tags are now implemented following the Tailscale model where tags and user ownership are mutually exclusive. Devices can be either user-owned (authenticated via web/OIDC) or tagged (authenticated via tagged PreAuthKeys). Tagged devices receive their identity from tags rather than users, making them suitable for servers and infrastructure. Applying a tag to a device removes user-based authentication. See the [Tailscale tags documentation](https://tailscale.com/kb/1068/tags) for details on how tags work.
2727

28-
User-owned nodes can now request tags during registration using `--advertise-tags`. Tags are validated against the `tagOwners` policy and applied at registration time. Tags are immutable after registration.
28+
User-owned nodes can now request tags during registration using `--advertise-tags`. Tags are validated against the `tagOwners` policy and applied at registration time. Tags can be managed via the CLI or API after registration.
2929

3030
### Database migration support removed for pre-0.25.0 databases
3131

@@ -42,7 +42,7 @@ release.
4242
- `--advertise-tags` is processed during registration, not on every policy evaluation
4343
- PreAuthKey tagged devices ignore `--advertise-tags` from clients
4444
- User-owned nodes can use `--advertise-tags` if authorized by `tagOwners` policy
45-
- Tags are immutable after registration
45+
- Tags can be managed via CLI (`headscale nodes tag`) or the SetTags API after registration
4646

4747
- Database migration support removed for pre-0.25.0 databases [#2883](https://github.com/juanfont/headscale/pull/2883)
4848
- If you are running a version older than 0.25.0, you must upgrade to 0.25.1 first, then upgrade to this release

hscontrol/policy/v2/policy.go

Lines changed: 98 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package v2
22

33
import (
4+
"cmp"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -560,12 +561,6 @@ func (pm *PolicyManager) NodeCanHaveTag(node types.NodeView, tag string) bool {
560561
return false
561562
}
562563

563-
// Tagged nodes can only keep tags they already have via client requests.
564-
// (Admin API bypasses this function entirely and can modify any tags.)
565-
if node.IsTagged() {
566-
return node.HasTag(tag)
567-
}
568-
569564
// Check if node's owner can assign this tag via the pre-resolved tagOwnerMap.
570565
// The tagOwnerMap contains IP sets built from resolving TagOwners entries
571566
// (usernames/groups) to their nodes' IPs, so checking if the node's IP
@@ -930,28 +925,116 @@ func (pm *PolicyManager) invalidateGlobalPolicyCache(newNodes views.Slice[types.
930925
}
931926
}
932927

928+
// flattenTags flattens the TagOwners by resolving nested tags and detecting cycles.
929+
// It will return a Owners list where all the Tag types have been resolved to their underlying Owners.
930+
func flattenTags(tagOwners TagOwners, tag Tag, visiting map[Tag]bool, chain []Tag) (Owners, error) {
931+
if visiting[tag] {
932+
cycleStart := 0
933+
934+
for i, t := range chain {
935+
if t == tag {
936+
cycleStart = i
937+
break
938+
}
939+
}
940+
941+
cycleTags := make([]string, len(chain[cycleStart:]))
942+
for i, t := range chain[cycleStart:] {
943+
cycleTags[i] = string(t)
944+
}
945+
946+
slices.Sort(cycleTags)
947+
948+
return nil, fmt.Errorf("%w: %s", ErrCircularReference, strings.Join(cycleTags, " -> "))
949+
}
950+
951+
visiting[tag] = true
952+
953+
chain = append(chain, tag)
954+
defer delete(visiting, tag)
955+
956+
var result Owners
957+
958+
for _, owner := range tagOwners[tag] {
959+
switch o := owner.(type) {
960+
case *Tag:
961+
if _, ok := tagOwners[*o]; !ok {
962+
return nil, fmt.Errorf("tag %q %w %q", tag, ErrUndefinedTagReference, *o)
963+
}
964+
965+
nested, err := flattenTags(tagOwners, *o, visiting, chain)
966+
if err != nil {
967+
return nil, err
968+
}
969+
970+
result = append(result, nested...)
971+
default:
972+
result = append(result, owner)
973+
}
974+
}
975+
976+
return result, nil
977+
}
978+
979+
// flattenTagOwners flattens all TagOwners by resolving nested tags and detecting cycles.
980+
// It will return a new TagOwners map where all the Tag types have been resolved to their underlying Owners.
981+
func flattenTagOwners(tagOwners TagOwners) (TagOwners, error) {
982+
ret := make(TagOwners)
983+
984+
for tag := range tagOwners {
985+
flattened, err := flattenTags(tagOwners, tag, make(map[Tag]bool), nil)
986+
if err != nil {
987+
return nil, err
988+
}
989+
990+
slices.SortFunc(flattened, func(a, b Owner) int {
991+
return cmp.Compare(a.String(), b.String())
992+
})
993+
ret[tag] = slices.CompactFunc(flattened, func(a, b Owner) bool {
994+
return a.String() == b.String()
995+
})
996+
}
997+
998+
return ret, nil
999+
}
1000+
9331001
// resolveTagOwners resolves the TagOwners to a map of Tag to netipx.IPSet.
9341002
// The resulting map can be used to quickly look up the IPSet for a given Tag.
9351003
// It is intended for internal use in a PolicyManager.
9361004
func resolveTagOwners(p *Policy, users types.Users, nodes views.Slice[types.NodeView]) (map[Tag]*netipx.IPSet, error) {
1005+
if p == nil {
1006+
return make(map[Tag]*netipx.IPSet), nil
1007+
}
1008+
1009+
if len(p.TagOwners) == 0 {
1010+
return make(map[Tag]*netipx.IPSet), nil
1011+
}
1012+
9371013
ret := make(map[Tag]*netipx.IPSet)
9381014

939-
if p == nil {
940-
return ret, nil
1015+
tagOwners, err := flattenTagOwners(p.TagOwners)
1016+
if err != nil {
1017+
return nil, err
9411018
}
9421019

943-
for tag, owners := range p.TagOwners {
1020+
for tag, owners := range tagOwners {
9441021
var ips netipx.IPSetBuilder
9451022

9461023
for _, owner := range owners {
947-
o, ok := owner.(Alias)
948-
if !ok {
949-
// Should never happen
1024+
switch o := owner.(type) {
1025+
case *Tag:
1026+
// After flattening, Tag types should not appear in the owners list.
1027+
// If they do, skip them as they represent already-resolved references.
1028+
1029+
case Alias:
1030+
// If it does not resolve, that means the tag is not associated with any IP addresses.
1031+
resolved, _ := o.Resolve(p, users, nodes)
1032+
ips.AddSet(resolved)
1033+
1034+
default:
1035+
// Should never happen - after flattening, all owners should be Alias types
9501036
return nil, fmt.Errorf("%w: %v", ErrInvalidTagOwner, owner)
9511037
}
952-
// If it does not resolve, that means the tag is not associated with any IP addresses.
953-
resolved, _ := o.Resolve(p, users, nodes)
954-
ips.AddSet(resolved)
9551038
}
9561039

9571040
ipSet, err := ips.IPSet()

hscontrol/policy/v2/types.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package v2
22

33
import (
4-
"cmp"
54
"errors"
65
"fmt"
76
"net/netip"

0 commit comments

Comments
 (0)