state: revalidate pre-auth key reuse when registration applies

This commit is contained in:
Kristoffer Dalby authored and Kristoffer Dalby committed 2026-10-07 14:03:54 +02:00
1 parent 00d64db9ef
commit c4ce3f7d14
8 files changed
+1005 -248

No files matched your search

+15 -1
View File
@@ -219,6 +219,11 @@ func (ns Nodes) ViewSlice() views.Slice[NodeView] {
// IsExpired returns whether the node registration has expired.
func (node *Node) IsExpired() bool {
return node.IsExpiredAt(time.Now())
}
// IsExpiredAt reports whether the node registration has expired at now.
func (node *Node) IsExpiredAt(now time.Time) bool {
// If Expiry is not set, the client has not indicated that
// it wants an expiry time, it is therefore considered
// to mean "not expired"
@@ -226,7 +231,7 @@ func (node *Node) IsExpired() bool {
return false
}
return time.Since(*node.Expiry) > 0
return now.After(*node.Expiry)
}
// Online reports the node's last known connectivity. Unknown counts as
@@ -933,6 +938,15 @@ func (nv NodeView) IsExpired() bool {
return nv.ж.IsExpired()
}
// IsExpiredAt reports whether the node registration has expired at now.
func (nv NodeView) IsExpiredAt(now time.Time) bool {
if !nv.Valid() {
return true
}
return nv.ж.IsExpiredAt(now)
}
// Online reports the node's last known connectivity.
func (nv NodeView) Online() bool {
if !nv.Valid() {
+30
View File
@@ -1043,3 +1043,33 @@ func TestHasPolicyChangeFields(t *testing.T) {
})
}
}
func TestNodeIsExpiredAt(t *testing.T) {
now := time.Now()
zero := time.Time{}
justBefore := now.Add(-time.Nanosecond)
tests := []struct {
name string
expiry *time.Time
want bool
}{
{name: "nil expiry", expiry: nil, want: false},
{name: "zero expiry", expiry: &zero, want: false},
{name: "expiry equals now", expiry: &now, want: false},
{name: "now one nanosecond after expiry", expiry: &justBefore, want: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n := &Node{Expiry: tt.expiry}
require.Equal(t, tt.want, n.IsExpiredAt(now))
require.Equal(t, tt.want, n.View().IsExpiredAt(now))
})
}
t.Run("invalid view", func(t *testing.T) {
require.True(t, NodeView{}.IsExpiredAt(now),
"an invalid view counts as expired, like IsExpired")
})
}
+34 -1
View File
@@ -98,11 +98,21 @@ func (pak *PreAuthKey) Validate() error {
EmbedObject(pak).
Msg("PreAuthKey.Validate: checking key")
return pak.ValidAt(time.Now())
}
// ValidAt is [PreAuthKey.Validate] at a caller-chosen instant, without
// logging, so a NodeStore writer can decide at its own clock.
func (pak *PreAuthKey) ValidAt(now time.Time) error {
if pak == nil {
return PAKError("invalid authkey")
}
if pak.Revoked != nil {
return PAKError("authkey revoked")
}
if pak.Expiration != nil && pak.Expiration.Before(time.Now()) {
if pak.Expiration != nil && pak.Expiration.Before(now) {
return PAKError("authkey expired")
}
@@ -124,6 +134,29 @@ func (pak *PreAuthKey) IsTagged() bool {
return len(pak.Tags) > 0
}
// Username returns the associated user's name, or TaggedDevices for a key
// without a user. For tagged keys the associated user is the creator.
func (pak *PreAuthKey) Username() string {
if pak.User != nil {
return pak.User.Username()
}
return TaggedDevices.Name
}
// ConvertsNodeToTagged reports whether this key changes a user-owned node
// to tag ownership.
func (pak *PreAuthKey) ConvertsNodeToTagged(node NodeView) bool {
return node.Valid() && pak.IsTagged() && !node.IsTagged()
}
// RetagsNode reports whether this key replaces an already-tagged node's
// tags. Reusing the last key preserves subsequent admin tag changes.
func (pak *PreAuthKey) RetagsNode(node NodeView) bool {
return node.IsTagged() && pak.IsTagged() &&
(!node.AuthKeyID().Valid() || node.AuthKeyID().Get() != pak.ID)
}
// maskedPrefix returns the key prefix in masked format for safe logging.
// SECURITY: Never log the full key or hash, only the masked prefix.
func (pak *PreAuthKey) maskedPrefix() string {
+75 -3
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)
func TestCanUsePreAuthKey(t *testing.T) {
@@ -14,11 +15,32 @@ func TestCanUsePreAuthKey(t *testing.T) {
future := now.Add(time.Hour)
tests := []struct {
name string
pak *PreAuthKey
name string
pak *PreAuthKey
// at, when set, checks ValidAt(at) instead of Validate.
at time.Time
wantErr bool
err PAKError
}{
{
name: "valid at the instant of expiration",
pak: &PreAuthKey{
Reusable: true,
Expiration: &now,
},
at: now,
wantErr: false,
},
{
name: "expired one nanosecond after expiration",
pak: &PreAuthKey{
Reusable: true,
Expiration: &now,
},
at: now.Add(time.Nanosecond),
wantErr: true,
err: PAKError("authkey expired"),
},
{
name: "valid reusable key",
pak: &PreAuthKey{
@@ -105,7 +127,16 @@ func TestCanUsePreAuthKey(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.pak.Validate()
var err error
if tt.at.IsZero() {
err = tt.pak.Validate()
if diff := cmp.Diff(err, tt.pak.ValidAt(time.Now())); diff != "" {
t.Errorf("Validate and ValidAt(now) disagree (-Validate +ValidAt):\n%s", diff)
}
} else {
err = tt.pak.ValidAt(tt.at)
}
if tt.wantErr {
if err == nil {
t.Errorf("expected error but got none")
@@ -127,3 +158,44 @@ func TestCanUsePreAuthKey(t *testing.T) {
})
}
}
func TestPreAuthKeyUsername(t *testing.T) {
user := &User{Name: "creator", Email: "creator@example.com"}
for _, pak := range []*PreAuthKey{
{User: user},
{User: user, Tags: []string{"tag:server"}},
} {
require.Equal(t, user.Username(), pak.Username())
}
require.Equal(t, TaggedDevices.Name, (&PreAuthKey{Tags: []string{"tag:server"}}).Username())
}
func TestPreAuthKeyTagChanges(t *testing.T) {
keyID := uint64(1)
userID := uint(1)
pak := &PreAuthKey{ID: keyID, Tags: []string{"tag:original"}, User: &User{ID: userID}}
userNode := (&Node{ID: 1, UserID: &userID}).View()
require.True(t, pak.ConvertsNodeToTagged(userNode))
require.False(t, pak.RetagsNode(userNode))
// An admin changed the tags, but the key identity is unchanged. The
// creator's UserID does not make this tagged node user-owned.
taggedNode := (&Node{
ID: 1, Tags: []string{"tag:admin"}, AuthKeyID: &keyID, UserID: &userID,
}).View()
require.False(t, pak.ConvertsNodeToTagged(taggedNode))
require.False(t, pak.RetagsNode(taggedNode))
otherKey := &PreAuthKey{ID: 2, Tags: []string{"tag:replacement"}}
require.True(t, otherKey.RetagsNode(taggedNode))
require.False(t, otherKey.ConvertsNodeToTagged(taggedNode))
require.True(t, pak.RetagsNode((&Node{ID: 1, Tags: []string{"tag:admin"}}).View()))
userKey := &PreAuthKey{User: &User{ID: userID}}
require.False(t, userKey.ConvertsNodeToTagged(userNode))
require.False(t, userKey.RetagsNode(taggedNode))
require.False(t, pak.ConvertsNodeToTagged(NodeView{}))
require.False(t, pak.RetagsNode(NodeView{}))
}