mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
177 lines
5.2 KiB
Go
177 lines
5.2 KiB
Go
package server
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
|
)
|
|
|
|
const testTTL = time.Minute
|
|
|
|
func unixCaller(uid uint32) ipcauth.Identity {
|
|
return ipcauth.KnownForTest(ipcauth.Identity{UID: uid, GID: uid})
|
|
}
|
|
|
|
func windowsCaller(sid string) ipcauth.Identity {
|
|
return ipcauth.KnownForTest(ipcauth.Identity{SID: sid})
|
|
}
|
|
|
|
func TestJWTCache_ServesTheOwner(t *testing.T) {
|
|
c := newJWTCache()
|
|
owner := unixCaller(1000)
|
|
c.store("token-for-1000", owner, testTTL, c.currentGeneration())
|
|
|
|
got, found := c.get(owner)
|
|
|
|
require.True(t, found, "the identity that stored the token must get it back")
|
|
assert.Equal(t, "token-for-1000", got)
|
|
}
|
|
|
|
// The disclosure this cache guards against: one local account collecting the
|
|
// SSH JWT another account's authentication put in the daemon-wide cache.
|
|
func TestJWTCache_RefusesAnotherLocalUser(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
owner ipcauth.Identity
|
|
caller ipcauth.Identity
|
|
}{
|
|
{"different uid", unixCaller(1000), unixCaller(65534)},
|
|
{"root is not the owner either", unixCaller(1000), unixCaller(0)},
|
|
{"different sid", windowsCaller("S-1-5-21-1-2-3-1001"), windowsCaller("S-1-5-21-1-2-3-1002")},
|
|
{"windows caller against a unix owner", unixCaller(0), windowsCaller("S-1-5-18")},
|
|
{"unix caller against a windows owner", windowsCaller("S-1-5-18"), unixCaller(0)},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := newJWTCache()
|
|
c.store("victim-token", tt.owner, testTTL, c.currentGeneration())
|
|
|
|
got, found := c.get(tt.caller)
|
|
|
|
assert.False(t, found, "a caller that is not the owner must get a miss")
|
|
assert.Empty(t, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJWTCache_EmptyCacheMatchesNobody(t *testing.T) {
|
|
c := newJWTCache()
|
|
|
|
got, found := c.get(unixCaller(0))
|
|
|
|
assert.False(t, found)
|
|
assert.Empty(t, got)
|
|
}
|
|
|
|
// An entry with no recorded owner must match nobody, root included: an
|
|
// unidentified caller arrives as the zero Identity, which carries uid 0. This
|
|
// pins the nil-owner guard rather than the comparison, so it sets up an entry
|
|
// that exists and then drops its owner.
|
|
func TestJWTCache_UnownedEntryMatchesNobody(t *testing.T) {
|
|
c := newJWTCache()
|
|
c.store("token", unixCaller(1000), testTTL, c.currentGeneration())
|
|
c.owner = nil
|
|
|
|
got, found := c.get(unixCaller(0))
|
|
|
|
assert.False(t, found)
|
|
assert.Empty(t, got)
|
|
}
|
|
|
|
// The same user calling once elevated and once not is still the same user, so
|
|
// hiding their own token from them would be wrong.
|
|
func TestJWTCache_ElevationDoesNotChangeTheOwner(t *testing.T) {
|
|
c := newJWTCache()
|
|
sid := "S-1-5-21-1-2-3-1001"
|
|
owner := windowsCaller(sid)
|
|
owner.Elevated = true
|
|
c.store("token", owner, testTTL, c.currentGeneration())
|
|
|
|
got, found := c.get(windowsCaller(sid))
|
|
|
|
require.True(t, found)
|
|
assert.Equal(t, "token", got)
|
|
}
|
|
|
|
func TestJWTCache_Expiry(t *testing.T) {
|
|
c := newJWTCache()
|
|
owner := unixCaller(1000)
|
|
c.store("token", owner, testTTL, c.currentGeneration())
|
|
c.expiresAt = time.Now().Add(-time.Second)
|
|
|
|
_, found := c.get(owner)
|
|
|
|
assert.False(t, found)
|
|
}
|
|
|
|
// Logout and SwitchProfile call clear — Down deliberately does not: the NetBird
|
|
// session the token speaks for is over, so not even its owner may have it back.
|
|
func TestJWTCache_ClearDropsTheEntry(t *testing.T) {
|
|
c := newJWTCache()
|
|
owner := unixCaller(1000)
|
|
c.store("token", owner, testTTL, c.currentGeneration())
|
|
|
|
c.clear()
|
|
|
|
_, found := c.get(owner)
|
|
assert.False(t, found)
|
|
assert.Nil(t, c.owner, "clear must forget the owner too")
|
|
assert.Nil(t, c.timer, "clear must stop the expiry timer")
|
|
}
|
|
|
|
// WaitJWTToken polls the IdP unlocked, so a logout or a profile switch can
|
|
// clear the cache while a flow is still in the air. The token that flow returns
|
|
// belongs to the session that ended, so it must not land in the cache the new
|
|
// session is using.
|
|
func TestJWTCache_StoreFromAnEndedSessionIsDropped(t *testing.T) {
|
|
c := newJWTCache()
|
|
owner := unixCaller(1000)
|
|
|
|
// The generation a caller takes when its authentication starts.
|
|
generation := c.currentGeneration()
|
|
|
|
c.clear() // logout or profile switch, while the IdP is still being polled
|
|
|
|
stored := c.store("stale-token", owner, testTTL, generation)
|
|
|
|
assert.False(t, stored, "a token from an ended session must not be cached")
|
|
_, found := c.get(owner)
|
|
assert.False(t, found, "the cache must stay empty after the session ended")
|
|
}
|
|
|
|
// The same caller must still be able to store once it re-reads the generation, so
|
|
// the guard does not wedge the cache after any invalidation.
|
|
func TestJWTCache_StoreWorksAgainAfterClear(t *testing.T) {
|
|
c := newJWTCache()
|
|
owner := unixCaller(1000)
|
|
|
|
c.clear()
|
|
|
|
require.True(t, c.store("token", owner, testTTL, c.currentGeneration()))
|
|
|
|
got, found := c.get(owner)
|
|
require.True(t, found)
|
|
assert.Equal(t, "token", got)
|
|
}
|
|
|
|
func TestJWTCache_StoreReplacesThePreviousOwner(t *testing.T) {
|
|
c := newJWTCache()
|
|
first := unixCaller(1000)
|
|
second := unixCaller(1001)
|
|
|
|
c.store("first-token", first, testTTL, c.currentGeneration())
|
|
c.store("second-token", second, testTTL, c.currentGeneration())
|
|
|
|
_, found := c.get(first)
|
|
assert.False(t, found, "the previous owner must not reach the new token")
|
|
|
|
got, found := c.get(second)
|
|
require.True(t, found)
|
|
assert.Equal(t, "second-token", got)
|
|
}
|