mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-23 17:18:50 +02:00
make tags first class node owner (#2885)
This PR changes tags to be something that exists on nodes in addition to users, to being its own thing. It is part of moving our tags support towards the correct tailscale compatible implementation. There are probably rough edges in this PR, but the intention is to get it in, and then start fixing bugs from 0.28.0 milestone (long standing tags issue) to discover what works and what doesnt. Updates #2417 Closes #2619
This commit is contained in:
@@ -6,7 +6,6 @@ import (
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -28,6 +27,7 @@ var (
|
||||
ErrHostnameTooLong = errors.New("hostname too long, cannot except 255 ASCII chars")
|
||||
ErrNodeHasNoGivenName = errors.New("node has no given name")
|
||||
ErrNodeUserHasNoName = errors.New("node user has no name")
|
||||
ErrCannotRemoveAllTags = errors.New("cannot remove all tags from node")
|
||||
|
||||
invalidDNSRegex = regexp.MustCompile("[^a-z0-9-.]+")
|
||||
)
|
||||
@@ -97,16 +97,21 @@ type Node struct {
|
||||
// GivenName is the name used in all DNS related
|
||||
// parts of headscale.
|
||||
GivenName string `gorm:"type:varchar(63);unique_index"`
|
||||
UserID uint
|
||||
User User `gorm:"constraint:OnDelete:CASCADE;"`
|
||||
|
||||
// UserID is set for ALL nodes (tagged and user-owned) to track "created by".
|
||||
// For tagged nodes, this is informational only - the tag is the owner.
|
||||
// For user-owned nodes, this identifies the owner.
|
||||
// Only nil for orphaned nodes (should not happen in normal operation).
|
||||
UserID *uint
|
||||
User *User `gorm:"constraint:OnDelete:CASCADE;"`
|
||||
|
||||
RegisterMethod string
|
||||
|
||||
// ForcedTags are tags set by CLI/API. It is not considered
|
||||
// the source of truth, but is one of the sources from
|
||||
// which a tag might originate.
|
||||
// ForcedTags are _always_ applied to the node.
|
||||
ForcedTags []string `gorm:"column:forced_tags;serializer:json"`
|
||||
// Tags is the definitive owner for tagged nodes.
|
||||
// When non-empty, the node is "tagged" and tags define its identity.
|
||||
// Empty for user-owned nodes.
|
||||
// Tags cannot be removed once set (one-way transition).
|
||||
Tags []string `gorm:"column:tags;serializer:json"`
|
||||
|
||||
// When a node has been created with a PreAuthKey, we need to
|
||||
// prevent the preauthkey from being deleted before the node.
|
||||
@@ -196,55 +201,32 @@ func (node *Node) HasIP(i netip.Addr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTagged reports if a device is tagged
|
||||
// and therefore should not be treated as a
|
||||
// user owned device.
|
||||
// Currently, this function only handles tags set
|
||||
// via CLI ("forced tags" and preauthkeys).
|
||||
// IsTagged reports if a device is tagged and therefore should not be treated
|
||||
// as a user-owned device.
|
||||
// When a node has tags, the tags define its identity (not the user).
|
||||
func (node *Node) IsTagged() bool {
|
||||
if len(node.ForcedTags) > 0 {
|
||||
return true
|
||||
}
|
||||
return len(node.Tags) > 0
|
||||
}
|
||||
|
||||
if node.AuthKey != nil && len(node.AuthKey.Tags) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if node.Hostinfo == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO(kradalby): Figure out how tagging should work
|
||||
// and hostinfo.requestedtags.
|
||||
// Do this in other work.
|
||||
|
||||
return false
|
||||
// IsUserOwned returns true if node is owned by a user (not tagged).
|
||||
// Tagged nodes may have a UserID for "created by" tracking, but the tag is the owner.
|
||||
func (node *Node) IsUserOwned() bool {
|
||||
return !node.IsTagged()
|
||||
}
|
||||
|
||||
// HasTag reports if a node has a given tag.
|
||||
// Currently, this function only handles tags set
|
||||
// via CLI ("forced tags" and preauthkeys).
|
||||
func (node *Node) HasTag(tag string) bool {
|
||||
return slices.Contains(node.Tags(), tag)
|
||||
return slices.Contains(node.Tags, tag)
|
||||
}
|
||||
|
||||
func (node *Node) Tags() []string {
|
||||
var tags []string
|
||||
|
||||
if node.AuthKey != nil {
|
||||
tags = append(tags, node.AuthKey.Tags...)
|
||||
// TypedUserID returns the UserID as a typed UserID type.
|
||||
// Returns 0 if UserID is nil.
|
||||
func (node *Node) TypedUserID() UserID {
|
||||
if node.UserID == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// TODO(kradalby): Figure out how tagging should work
|
||||
// and hostinfo.requestedtags.
|
||||
// Do this in other work.
|
||||
// #2417
|
||||
|
||||
tags = append(tags, node.ForcedTags...)
|
||||
sort.Strings(tags)
|
||||
tags = slices.Compact(tags)
|
||||
|
||||
return tags
|
||||
return UserID(*node.UserID)
|
||||
}
|
||||
|
||||
func (node *Node) RequestTags() []string {
|
||||
@@ -389,8 +371,8 @@ func (node *Node) Proto() *v1.Node {
|
||||
IpAddresses: node.IPsAsString(),
|
||||
Name: node.Hostname,
|
||||
GivenName: node.GivenName,
|
||||
User: node.User.Proto(),
|
||||
ForcedTags: node.ForcedTags,
|
||||
User: nil, // Will be set below based on node type
|
||||
ForcedTags: node.Tags,
|
||||
Online: node.IsOnline != nil && *node.IsOnline,
|
||||
|
||||
// Only ApprovedRoutes and AvailableRoutes is set here. SubnetRoutes has
|
||||
@@ -404,6 +386,13 @@ func (node *Node) Proto() *v1.Node {
|
||||
CreatedAt: timestamppb.New(node.CreatedAt),
|
||||
}
|
||||
|
||||
// Set User field based on node ownership
|
||||
// Note: User will be set to TaggedDevices in the gRPC layer (grpcv1.go)
|
||||
// for proper MapResponse formatting
|
||||
if node.User != nil {
|
||||
nodeProto.User = node.User.Proto()
|
||||
}
|
||||
|
||||
if node.AuthKey != nil {
|
||||
nodeProto.PreAuthKey = node.AuthKey.Proto()
|
||||
}
|
||||
@@ -701,8 +690,20 @@ func (nodes Nodes) DebugString() string {
|
||||
func (node Node) DebugString() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "%s(%s):\n", node.Hostname, node.ID)
|
||||
fmt.Fprintf(&sb, "\tUser: %s (%d, %q)\n", node.User.Display(), node.User.ID, node.User.Username())
|
||||
fmt.Fprintf(&sb, "\tTags: %v\n", node.Tags())
|
||||
|
||||
// Show ownership status
|
||||
if node.IsTagged() {
|
||||
fmt.Fprintf(&sb, "\tTagged: %v\n", node.Tags)
|
||||
|
||||
if node.User != nil {
|
||||
fmt.Fprintf(&sb, "\tCreated by: %s (%d, %q)\n", node.User.Display(), node.User.ID, node.User.Username())
|
||||
}
|
||||
} else if node.User != nil {
|
||||
fmt.Fprintf(&sb, "\tUser-owned: %s (%d, %q)\n", node.User.Display(), node.User.ID, node.User.Username())
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "\tOrphaned: no user or tags\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(&sb, "\tIPs: %v\n", node.IPs())
|
||||
fmt.Fprintf(&sb, "\tApprovedRoutes: %v\n", node.ApprovedRoutes)
|
||||
fmt.Fprintf(&sb, "\tAnnouncedRoutes: %v\n", node.AnnouncedRoutes())
|
||||
@@ -714,8 +715,7 @@ func (node Node) DebugString() string {
|
||||
}
|
||||
|
||||
func (v NodeView) UserView() UserView {
|
||||
u := v.User()
|
||||
return u.View()
|
||||
return v.User()
|
||||
}
|
||||
|
||||
func (v NodeView) IPs() []netip.Addr {
|
||||
@@ -790,13 +790,6 @@ func (v NodeView) RequestTagsSlice() views.Slice[string] {
|
||||
return v.Hostinfo().RequestTags()
|
||||
}
|
||||
|
||||
func (v NodeView) Tags() []string {
|
||||
if !v.Valid() {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Tags()
|
||||
}
|
||||
|
||||
// IsTagged reports if a device is tagged
|
||||
// and therefore should not be treated as a
|
||||
// user owned device.
|
||||
@@ -893,6 +886,32 @@ func (v NodeView) HasTag(tag string) bool {
|
||||
return v.ж.HasTag(tag)
|
||||
}
|
||||
|
||||
// TypedUserID returns the UserID as a typed UserID type.
|
||||
// Returns 0 if UserID is nil or node is invalid.
|
||||
func (v NodeView) TypedUserID() UserID {
|
||||
if !v.Valid() {
|
||||
return 0
|
||||
}
|
||||
|
||||
return v.ж.TypedUserID()
|
||||
}
|
||||
|
||||
// TailscaleUserID returns the user ID to use in Tailscale protocol.
|
||||
// Tagged nodes always return TaggedDevices.ID, user-owned nodes return their actual UserID.
|
||||
func (v NodeView) TailscaleUserID() tailcfg.UserID {
|
||||
if !v.Valid() {
|
||||
return 0
|
||||
}
|
||||
|
||||
if v.IsTagged() {
|
||||
//nolint:gosec // G115: TaggedDevices.ID is a constant that fits in int64
|
||||
return tailcfg.UserID(int64(TaggedDevices.ID))
|
||||
}
|
||||
|
||||
//nolint:gosec // G115: UserID values are within int64 range
|
||||
return tailcfg.UserID(int64(v.UserID().Get()))
|
||||
}
|
||||
|
||||
// Prefixes returns the node IPs as netip.Prefix.
|
||||
func (v NodeView) Prefixes() []netip.Prefix {
|
||||
if !v.Valid() {
|
||||
|
||||
295
hscontrol/types/node_tags_test.go
Normal file
295
hscontrol/types/node_tags_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/util"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// TestNodeIsTagged tests the IsTagged() method for determining if a node is tagged.
|
||||
func TestNodeIsTagged(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node Node
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "node with tags - is tagged",
|
||||
node: Node{
|
||||
Tags: []string{"tag:server", "tag:prod"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "node with single tag - is tagged",
|
||||
node: Node{
|
||||
Tags: []string{"tag:web"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "node with no tags - not tagged",
|
||||
node: Node{
|
||||
Tags: []string{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "node with nil tags - not tagged",
|
||||
node: Node{
|
||||
Tags: nil,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
// Tags should be copied from AuthKey during registration, so a node
|
||||
// with only AuthKey.Tags and no Tags would be invalid in practice.
|
||||
// IsTagged() only checks node.Tags, not AuthKey.Tags.
|
||||
name: "node registered with tagged authkey only - not tagged (tags should be copied)",
|
||||
node: Node{
|
||||
AuthKey: &PreAuthKey{
|
||||
Tags: []string{"tag:database"},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "node with both tags and authkey tags - is tagged",
|
||||
node: Node{
|
||||
Tags: []string{"tag:server"},
|
||||
AuthKey: &PreAuthKey{
|
||||
Tags: []string{"tag:database"},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "node with user and no tags - not tagged",
|
||||
node: Node{
|
||||
UserID: ptr.To(uint(42)),
|
||||
Tags: []string{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.node.IsTagged()
|
||||
assert.Equal(t, tt.want, got, "IsTagged() returned unexpected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodeViewIsTagged tests the IsTagged() method on NodeView.
|
||||
func TestNodeViewIsTagged(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node Node
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "tagged node via Tags field",
|
||||
node: Node{
|
||||
Tags: []string{"tag:server"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
// Tags should be copied from AuthKey during registration, so a node
|
||||
// with only AuthKey.Tags and no Tags would be invalid in practice.
|
||||
name: "node with only AuthKey tags - not tagged (tags should be copied)",
|
||||
node: Node{
|
||||
AuthKey: &PreAuthKey{
|
||||
Tags: []string{"tag:web"},
|
||||
},
|
||||
},
|
||||
want: false, // IsTagged() only checks node.Tags
|
||||
},
|
||||
{
|
||||
name: "user-owned node",
|
||||
node: Node{
|
||||
UserID: ptr.To(uint(1)),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
view := tt.node.View()
|
||||
got := view.IsTagged()
|
||||
assert.Equal(t, tt.want, got, "NodeView.IsTagged() returned unexpected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodeHasTag tests the HasTag() method for checking specific tag membership.
|
||||
func TestNodeHasTag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node Node
|
||||
tag string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "node has the tag",
|
||||
node: Node{
|
||||
Tags: []string{"tag:server", "tag:prod"},
|
||||
},
|
||||
tag: "tag:server",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "node does not have the tag",
|
||||
node: Node{
|
||||
Tags: []string{"tag:server", "tag:prod"},
|
||||
},
|
||||
tag: "tag:web",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
// Tags should be copied from AuthKey during registration
|
||||
// HasTag() only checks node.Tags, not AuthKey.Tags
|
||||
name: "node has tag only in authkey - returns false",
|
||||
node: Node{
|
||||
AuthKey: &PreAuthKey{
|
||||
Tags: []string{"tag:database"},
|
||||
},
|
||||
},
|
||||
tag: "tag:database",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
// node.Tags is what matters, not AuthKey.Tags
|
||||
name: "node has tag in Tags but not in AuthKey",
|
||||
node: Node{
|
||||
Tags: []string{"tag:server"},
|
||||
AuthKey: &PreAuthKey{
|
||||
Tags: []string{"tag:database"},
|
||||
},
|
||||
},
|
||||
tag: "tag:server",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invalid tag format still returns false",
|
||||
node: Node{
|
||||
Tags: []string{"tag:server"},
|
||||
},
|
||||
tag: "invalid-tag",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty tag returns false",
|
||||
node: Node{
|
||||
Tags: []string{"tag:server"},
|
||||
},
|
||||
tag: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.node.HasTag(tt.tag)
|
||||
assert.Equal(t, tt.want, got, "HasTag() returned unexpected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNodeTagsImmutableAfterRegistration tests that tags can only be set during registration.
|
||||
func TestNodeTagsImmutableAfterRegistration(t *testing.T) {
|
||||
// Test that a node registered with tags keeps them
|
||||
taggedNode := Node{
|
||||
ID: 1,
|
||||
Tags: []string{"tag:server"},
|
||||
AuthKey: &PreAuthKey{
|
||||
Tags: []string{"tag:server"},
|
||||
},
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
}
|
||||
|
||||
// Node should be tagged
|
||||
assert.True(t, taggedNode.IsTagged(), "Node registered with tags should be tagged")
|
||||
|
||||
// Node should have the tag
|
||||
has := taggedNode.HasTag("tag:server")
|
||||
assert.True(t, has, "Node should have the tag it was registered with")
|
||||
|
||||
// Test that a user-owned node is not tagged
|
||||
userNode := Node{
|
||||
ID: 2,
|
||||
UserID: ptr.To(uint(42)),
|
||||
Tags: []string{},
|
||||
RegisterMethod: util.RegisterMethodOIDC,
|
||||
}
|
||||
|
||||
assert.False(t, userNode.IsTagged(), "User-owned node should not be tagged")
|
||||
}
|
||||
|
||||
// TestNodeOwnershipModel tests the tags-as-identity model.
|
||||
func TestNodeOwnershipModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
node Node
|
||||
wantIsTagged bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "tagged node has tags, UserID is informational",
|
||||
node: Node{
|
||||
ID: 1,
|
||||
UserID: ptr.To(uint(5)), // "created by" user 5
|
||||
Tags: []string{"tag:server"},
|
||||
},
|
||||
wantIsTagged: true,
|
||||
description: "Tagged nodes may have UserID set for tracking, but ownership is defined by tags",
|
||||
},
|
||||
{
|
||||
name: "user-owned node has no tags",
|
||||
node: Node{
|
||||
ID: 2,
|
||||
UserID: ptr.To(uint(5)),
|
||||
Tags: []string{},
|
||||
},
|
||||
wantIsTagged: false,
|
||||
description: "User-owned nodes are owned by the user, not by tags",
|
||||
},
|
||||
{
|
||||
// Tags should be copied from AuthKey to Node during registration
|
||||
// IsTagged() only checks node.Tags, not AuthKey.Tags
|
||||
name: "node with only authkey tags - not tagged (tags should be copied)",
|
||||
node: Node{
|
||||
ID: 3,
|
||||
UserID: ptr.To(uint(5)), // "created by" user 5
|
||||
AuthKey: &PreAuthKey{
|
||||
Tags: []string{"tag:database"},
|
||||
},
|
||||
},
|
||||
wantIsTagged: false,
|
||||
description: "IsTagged() only checks node.Tags; AuthKey.Tags should be copied during registration",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.node.IsTagged()
|
||||
assert.Equal(t, tt.wantIsTagged, got, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserTypedID tests the TypedID() helper method.
|
||||
func TestUserTypedID(t *testing.T) {
|
||||
user := User{
|
||||
Model: gorm.Model{ID: 42},
|
||||
}
|
||||
|
||||
typedID := user.TypedID()
|
||||
assert.NotNil(t, typedID, "TypedID() should return non-nil pointer")
|
||||
assert.Equal(t, UserID(42), *typedID, "TypedID() should return correct UserID value")
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func TestNodeFQDN(t *testing.T) {
|
||||
name: "no-dnsconfig-with-username",
|
||||
node: Node{
|
||||
GivenName: "test",
|
||||
User: User{
|
||||
User: &User{
|
||||
Name: "user",
|
||||
},
|
||||
},
|
||||
@@ -150,7 +150,7 @@ func TestNodeFQDN(t *testing.T) {
|
||||
name: "all-set",
|
||||
node: Node{
|
||||
GivenName: "test",
|
||||
User: User{
|
||||
User: &User{
|
||||
Name: "user",
|
||||
},
|
||||
},
|
||||
@@ -160,7 +160,7 @@ func TestNodeFQDN(t *testing.T) {
|
||||
{
|
||||
name: "no-given-name",
|
||||
node: Node{
|
||||
User: User{
|
||||
User: &User{
|
||||
Name: "user",
|
||||
},
|
||||
},
|
||||
@@ -179,7 +179,7 @@ func TestNodeFQDN(t *testing.T) {
|
||||
name: "no-dnsconfig",
|
||||
node: Node{
|
||||
GivenName: "test",
|
||||
User: User{
|
||||
User: &User{
|
||||
Name: "user",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -23,16 +23,19 @@ type PreAuthKey struct {
|
||||
Prefix string
|
||||
Hash []byte // bcrypt
|
||||
|
||||
UserID uint
|
||||
User User `gorm:"constraint:OnDelete:SET NULL;"`
|
||||
// For tagged keys: UserID tracks who created the key (informational)
|
||||
// For user-owned keys: UserID tracks the node owner
|
||||
// Can be nil for system-created tagged keys
|
||||
UserID *uint
|
||||
User *User `gorm:"constraint:OnDelete:SET NULL;"`
|
||||
|
||||
Reusable bool
|
||||
Ephemeral bool `gorm:"default:false"`
|
||||
Used bool `gorm:"default:false"`
|
||||
|
||||
// Tags are always applied to the node and is one of
|
||||
// the sources of tags a node might have. They are copied
|
||||
// from the PreAuthKey when the node logs in the first time,
|
||||
// and ignored after.
|
||||
// Tags to assign to nodes registered with this key.
|
||||
// Tags are copied to the node during registration.
|
||||
// If non-empty, this creates tagged nodes (not user-owned).
|
||||
Tags []string `gorm:"serializer:json"`
|
||||
|
||||
CreatedAt *time.Time
|
||||
@@ -48,19 +51,23 @@ type PreAuthKeyNew struct {
|
||||
Tags []string
|
||||
Expiration *time.Time
|
||||
CreatedAt *time.Time
|
||||
User User
|
||||
User *User // Can be nil for system-created tagged keys
|
||||
}
|
||||
|
||||
func (key *PreAuthKeyNew) Proto() *v1.PreAuthKey {
|
||||
protoKey := v1.PreAuthKey{
|
||||
Id: key.ID,
|
||||
Key: key.Key,
|
||||
User: key.User.Proto(),
|
||||
User: nil, // Will be set below if not nil
|
||||
Reusable: key.Reusable,
|
||||
Ephemeral: key.Ephemeral,
|
||||
AclTags: key.Tags,
|
||||
}
|
||||
|
||||
if key.User != nil {
|
||||
protoKey.User = key.User.Proto()
|
||||
}
|
||||
|
||||
if key.Expiration != nil {
|
||||
protoKey.Expiration = timestamppb.New(*key.Expiration)
|
||||
}
|
||||
@@ -74,7 +81,7 @@ func (key *PreAuthKeyNew) Proto() *v1.PreAuthKey {
|
||||
|
||||
func (key *PreAuthKey) Proto() *v1.PreAuthKey {
|
||||
protoKey := v1.PreAuthKey{
|
||||
User: key.User.Proto(),
|
||||
User: nil, // Will be set below if not nil
|
||||
Id: key.ID,
|
||||
Ephemeral: key.Ephemeral,
|
||||
Reusable: key.Reusable,
|
||||
@@ -82,6 +89,10 @@ func (key *PreAuthKey) Proto() *v1.PreAuthKey {
|
||||
AclTags: key.Tags,
|
||||
}
|
||||
|
||||
if key.User != nil {
|
||||
protoKey.User = key.User.Proto()
|
||||
}
|
||||
|
||||
// For new keys (with prefix/hash), show the prefix so users can identify the key
|
||||
// For legacy keys (with plaintext key), show the full key for backwards compatibility
|
||||
if key.Prefix != "" {
|
||||
@@ -139,3 +150,9 @@ func (pak *PreAuthKey) Validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTagged returns true if this PreAuthKey creates tagged nodes.
|
||||
// When a PreAuthKey has tags, nodes registered with it will be tagged nodes.
|
||||
func (pak *PreAuthKey) IsTagged() bool {
|
||||
return len(pak.Tags) > 0
|
||||
}
|
||||
|
||||
@@ -54,7 +54,13 @@ func (src *Node) Clone() *Node {
|
||||
if dst.IPv6 != nil {
|
||||
dst.IPv6 = ptr.To(*src.IPv6)
|
||||
}
|
||||
dst.ForcedTags = append(src.ForcedTags[:0:0], src.ForcedTags...)
|
||||
if dst.UserID != nil {
|
||||
dst.UserID = ptr.To(*src.UserID)
|
||||
}
|
||||
if dst.User != nil {
|
||||
dst.User = ptr.To(*src.User)
|
||||
}
|
||||
dst.Tags = append(src.Tags[:0:0], src.Tags...)
|
||||
if dst.AuthKeyID != nil {
|
||||
dst.AuthKeyID = ptr.To(*src.AuthKeyID)
|
||||
}
|
||||
@@ -87,10 +93,10 @@ var _NodeCloneNeedsRegeneration = Node(struct {
|
||||
IPv6 *netip.Addr
|
||||
Hostname string
|
||||
GivenName string
|
||||
UserID uint
|
||||
User User
|
||||
UserID *uint
|
||||
User *User
|
||||
RegisterMethod string
|
||||
ForcedTags []string
|
||||
Tags []string
|
||||
AuthKeyID *uint64
|
||||
AuthKey *PreAuthKey
|
||||
Expiry *time.Time
|
||||
@@ -111,6 +117,12 @@ func (src *PreAuthKey) Clone() *PreAuthKey {
|
||||
dst := new(PreAuthKey)
|
||||
*dst = *src
|
||||
dst.Hash = append(src.Hash[:0:0], src.Hash...)
|
||||
if dst.UserID != nil {
|
||||
dst.UserID = ptr.To(*src.UserID)
|
||||
}
|
||||
if dst.User != nil {
|
||||
dst.User = ptr.To(*src.User)
|
||||
}
|
||||
dst.Tags = append(src.Tags[:0:0], src.Tags...)
|
||||
if dst.CreatedAt != nil {
|
||||
dst.CreatedAt = ptr.To(*src.CreatedAt)
|
||||
@@ -127,8 +139,8 @@ var _PreAuthKeyCloneNeedsRegeneration = PreAuthKey(struct {
|
||||
Key string
|
||||
Prefix string
|
||||
Hash []byte
|
||||
UserID uint
|
||||
User User
|
||||
UserID *uint
|
||||
User *User
|
||||
Reusable bool
|
||||
Ephemeral bool
|
||||
Used bool
|
||||
|
||||
@@ -139,12 +139,13 @@ func (v NodeView) IPv4() views.ValuePointer[netip.Addr] { return views.ValuePo
|
||||
|
||||
func (v NodeView) IPv6() views.ValuePointer[netip.Addr] { return views.ValuePointerOf(v.ж.IPv6) }
|
||||
|
||||
func (v NodeView) Hostname() string { return v.ж.Hostname }
|
||||
func (v NodeView) GivenName() string { return v.ж.GivenName }
|
||||
func (v NodeView) UserID() uint { return v.ж.UserID }
|
||||
func (v NodeView) User() User { return v.ж.User }
|
||||
func (v NodeView) Hostname() string { return v.ж.Hostname }
|
||||
func (v NodeView) GivenName() string { return v.ж.GivenName }
|
||||
func (v NodeView) UserID() views.ValuePointer[uint] { return views.ValuePointerOf(v.ж.UserID) }
|
||||
|
||||
func (v NodeView) User() UserView { return v.ж.User.View() }
|
||||
func (v NodeView) RegisterMethod() string { return v.ж.RegisterMethod }
|
||||
func (v NodeView) ForcedTags() views.Slice[string] { return views.SliceOf(v.ж.ForcedTags) }
|
||||
func (v NodeView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
|
||||
func (v NodeView) AuthKeyID() views.ValuePointer[uint64] { return views.ValuePointerOf(v.ж.AuthKeyID) }
|
||||
|
||||
func (v NodeView) AuthKey() PreAuthKeyView { return v.ж.AuthKey.View() }
|
||||
@@ -179,10 +180,10 @@ var _NodeViewNeedsRegeneration = Node(struct {
|
||||
IPv6 *netip.Addr
|
||||
Hostname string
|
||||
GivenName string
|
||||
UserID uint
|
||||
User User
|
||||
UserID *uint
|
||||
User *User
|
||||
RegisterMethod string
|
||||
ForcedTags []string
|
||||
Tags []string
|
||||
AuthKeyID *uint64
|
||||
AuthKey *PreAuthKey
|
||||
Expiry *time.Time
|
||||
@@ -239,16 +240,17 @@ func (v *PreAuthKeyView) UnmarshalJSON(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v PreAuthKeyView) ID() uint64 { return v.ж.ID }
|
||||
func (v PreAuthKeyView) Key() string { return v.ж.Key }
|
||||
func (v PreAuthKeyView) Prefix() string { return v.ж.Prefix }
|
||||
func (v PreAuthKeyView) Hash() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.Hash) }
|
||||
func (v PreAuthKeyView) UserID() uint { return v.ж.UserID }
|
||||
func (v PreAuthKeyView) User() User { return v.ж.User }
|
||||
func (v PreAuthKeyView) Reusable() bool { return v.ж.Reusable }
|
||||
func (v PreAuthKeyView) Ephemeral() bool { return v.ж.Ephemeral }
|
||||
func (v PreAuthKeyView) Used() bool { return v.ж.Used }
|
||||
func (v PreAuthKeyView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
|
||||
func (v PreAuthKeyView) ID() uint64 { return v.ж.ID }
|
||||
func (v PreAuthKeyView) Key() string { return v.ж.Key }
|
||||
func (v PreAuthKeyView) Prefix() string { return v.ж.Prefix }
|
||||
func (v PreAuthKeyView) Hash() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.Hash) }
|
||||
func (v PreAuthKeyView) UserID() views.ValuePointer[uint] { return views.ValuePointerOf(v.ж.UserID) }
|
||||
|
||||
func (v PreAuthKeyView) User() UserView { return v.ж.User.View() }
|
||||
func (v PreAuthKeyView) Reusable() bool { return v.ж.Reusable }
|
||||
func (v PreAuthKeyView) Ephemeral() bool { return v.ж.Ephemeral }
|
||||
func (v PreAuthKeyView) Used() bool { return v.ж.Used }
|
||||
func (v PreAuthKeyView) Tags() views.Slice[string] { return views.SliceOf(v.ж.Tags) }
|
||||
func (v PreAuthKeyView) CreatedAt() views.ValuePointer[time.Time] {
|
||||
return views.ValuePointerOf(v.ж.CreatedAt)
|
||||
}
|
||||
@@ -263,8 +265,8 @@ var _PreAuthKeyViewNeedsRegeneration = PreAuthKey(struct {
|
||||
Key string
|
||||
Prefix string
|
||||
Hash []byte
|
||||
UserID uint
|
||||
User User
|
||||
UserID *uint
|
||||
User *User
|
||||
Reusable bool
|
||||
Ephemeral bool
|
||||
Used bool
|
||||
|
||||
@@ -22,6 +22,21 @@ type UserID uint64
|
||||
|
||||
type Users []User
|
||||
|
||||
const (
|
||||
// TaggedDevicesUserID is the special user ID for tagged devices.
|
||||
// This ID is used when rendering tagged nodes in the Tailscale protocol.
|
||||
TaggedDevicesUserID = 2147455555
|
||||
)
|
||||
|
||||
// TaggedDevices is a special user used in MapResponse for tagged nodes.
|
||||
// Tagged nodes don't belong to a real user - the tag is their identity.
|
||||
// This special user ID is used when rendering tagged nodes in the Tailscale protocol.
|
||||
var TaggedDevices = User{
|
||||
Model: gorm.Model{ID: TaggedDevicesUserID},
|
||||
Name: "tagged-devices",
|
||||
DisplayName: "Tagged Devices",
|
||||
}
|
||||
|
||||
func (u Users) String() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[ ")
|
||||
@@ -77,6 +92,13 @@ func (u *User) StringID() string {
|
||||
return strconv.FormatUint(uint64(u.ID), 10)
|
||||
}
|
||||
|
||||
// TypedID returns a pointer to the user's ID as a UserID type.
|
||||
// This is a convenience method to avoid ugly casting like ptr.To(types.UserID(user.ID)).
|
||||
func (u *User) TypedID() *UserID {
|
||||
uid := UserID(u.ID)
|
||||
return &uid
|
||||
}
|
||||
|
||||
// Username is the main way to get the username of a user,
|
||||
// it will return the email if it exists, the name if it exists,
|
||||
// the OIDCIdentifier if it exists, and the ID if nothing else exists.
|
||||
@@ -117,6 +139,13 @@ func (u UserView) TailscaleUser() tailcfg.User {
|
||||
return u.ж.TailscaleUser()
|
||||
}
|
||||
|
||||
// ID returns the user's ID.
|
||||
// This is a custom accessor because gorm.Model.ID is embedded
|
||||
// and the viewer generator doesn't always produce it.
|
||||
func (u UserView) ID() uint {
|
||||
return u.ж.ID
|
||||
}
|
||||
|
||||
func (u *User) TailscaleLogin() tailcfg.Login {
|
||||
return tailcfg.Login{
|
||||
ID: tailcfg.LoginID(u.ID),
|
||||
|
||||
Reference in New Issue
Block a user