mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-23 14:56:49 +09:00
state: revalidate pre-auth key reuse when registration applies
This commit is contained in:
1 parent
00d64db9ef
commit
c4ce3f7d14
8 files changed
+1005
-248
No files matched your search
+15
-1
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{}))
|
||||
}
|
||||
Reference in new issue
Block a user