Describe session auth as SSH and VNC, cover the conflicting key path, fix de and ru wording

This commit is contained in:
Viktor Liu
2026-08-27 21:08:20 +02:00
parent 913a1c6033
commit f5d821ccb2
4 changed files with 51 additions and 7 deletions

View File

@@ -27,7 +27,9 @@ var (
ErrSessionKeyNotKnown = errors.New("session pubkey not registered")
)
// Authorizer handles SSH fine-grained access control authorization
// Authorizer handles fine-grained access control authorization for the
// remote-access servers: SSH by hashed user identity and OS login name, VNC by
// the session public key management issued for a temporary-access grant.
type Authorizer struct {
// UserIDClaim is the JWT claim to extract the user ID from
userIDClaim string
@@ -53,7 +55,7 @@ type Authorizer struct {
mu sync.RWMutex
}
// Config contains configuration for the SSH authorizer
// Config contains configuration for the session authorizer
type Config struct {
// UserIDClaim is the JWT claim to extract the user ID from (e.g., "sub", "email")
UserIDClaim string
@@ -80,7 +82,7 @@ type SessionPubKey struct {
DisplayName string
}
// NewAuthorizer creates a new SSH authorizer with empty configuration
// NewAuthorizer creates a new session authorizer with empty configuration
func NewAuthorizer() *Authorizer {
a := &Authorizer{
userIDClaim: DefaultUserIDClaim,

View File

@@ -2,6 +2,7 @@ package sessionauth
import (
"errors"
"sync"
"testing"
"github.com/stretchr/testify/assert"
@@ -399,11 +400,27 @@ func TestAuthorizer_ConcurrentAuthorization(t *testing.T) {
}(i)
}
// Reapply the same config while the reads above are in flight: Update
// replaces authorizedUsers, machineUsers and sessionPubKeys wholesale, so
// the race detector has something to catch if any of them is read unguarded.
const numUpdaters = 10
var updaters sync.WaitGroup
updaters.Add(numUpdaters)
for range numUpdaters {
go func() {
defer updaters.Done()
for range 50 {
authorizer.Update(config)
}
}()
}
// Wait for all goroutines to complete and collect errors
for i := 0; i < numGoroutines; i++ {
err := <-errChan
assert.NoError(t, err)
}
updaters.Wait()
}
func TestAuthorizer_Wildcard_AllowsAllAuthorizedUsers(t *testing.T) {
@@ -662,6 +679,31 @@ func TestAuthorizer_LookupSessionKey_UpdateClears(t *testing.T) {
}
}
// A session pubkey configured twice with different user hashes cannot be
// resolved to one identity, so Update drops the binding rather than picking a
// winner: the key must not authenticate anybody.
func TestAuthorizer_SessionKey_ConflictingDuplicateIsDropped(t *testing.T) {
pub := bytesRepeat(0x44, sessionPubKeyLen)
aliceHash, err := sshauth.HashUserID("alice")
require.NoError(t, err)
bobHash, err := sshauth.HashUserID("bob")
require.NoError(t, err)
a := NewAuthorizer()
a.Update(&Config{
AuthorizedUsers: []sshauth.UserIDHash{aliceHash, bobHash},
MachineUsers: map[string][]uint32{Wildcard: {0, 1}},
SessionPubKeys: []SessionPubKey{
{PubKey: pub, UserIDHash: aliceHash, DisplayName: "Alice"},
{PubKey: pub, UserIDHash: bobHash, DisplayName: "Bob"},
},
})
_, err = a.LookupSessionKey(pub)
require.ErrorIs(t, err, ErrSessionKeyNotKnown, "a conflicting key must authenticate nobody")
assert.Empty(t, a.LookupSessionDisplayName(pub), "the display name goes with the dropped binding")
}
func bytesRepeat(b byte, n int) []byte {
out := make([]byte, n)
for i := range out {