diff --git a/client/ui/i18n/locales/de/common.json b/client/ui/i18n/locales/de/common.json index db662e763..129b9fae9 100644 --- a/client/ui/i18n/locales/de/common.json +++ b/client/ui/i18n/locales/de/common.json @@ -1076,7 +1076,7 @@ "message": "Eingehende Verbindung zulassen?" }, "approval.field.user": { - "message": "Von Benutzer" + "message": "Vom Benutzer" }, "approval.field.keyFingerprint": { "message": "Schlüssel-Fingerabdruck" diff --git a/client/ui/i18n/locales/ru/common.json b/client/ui/i18n/locales/ru/common.json index c008e850c..1fd939cf9 100644 --- a/client/ui/i18n/locales/ru/common.json +++ b/client/ui/i18n/locales/ru/common.json @@ -1427,15 +1427,15 @@ "message": "Ожидание авторизации…" }, "connect.activeSession.badge": { - "message": "Экран доступен", + "message": "Экран транслируется", "description": "Short label on the badge shown on the main screen while somebody is attached to this machine over VNC." }, "connect.activeSession.tooltip": { - "message": "Этот экран просматривается по VNC ({sessionCount} сеанс(ов)). Отключение завершит его, и если вы подключены через VNC, вы потеряете доступ.", + "message": "Этот экран сейчас просматривают по VNC ({sessionCount} сеанс(ов)). Отключение завершит VNC-сеанс, и если вы подключены через VNC, вы потеряете доступ.", "description": "Tooltip on the screen-shared badge. {sessionCount} is how many VNC sessions are attached; wording covers any number since the bundle has no plural forms." }, "connect.activeSession.tooltipNamed": { - "message": "Этот экран просматривает {who} по VNC ({sessionCount} сеанс(ов)). Отключение завершит его, и если вы подключены через VNC, вы потеряете доступ.", + "message": "Этот экран сейчас просматривает {who} по VNC ({sessionCount} сеанс(ов)). Отключение завершит VNC-сеанс, и если вы подключены через VNC, вы потеряете доступ.", "description": "As connect.activeSession.tooltip, with {who} naming the dashboard users who started the sessions." }, "settings.privilege.hint": { diff --git a/shared/sessionauth/auth.go b/shared/sessionauth/auth.go index f69e1869b..1779fb5f5 100644 --- a/shared/sessionauth/auth.go +++ b/shared/sessionauth/auth.go @@ -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, diff --git a/shared/sessionauth/auth_test.go b/shared/sessionauth/auth_test.go index 6c5395f4e..b9a47d02d 100644 --- a/shared/sessionauth/auth_test.go +++ b/shared/sessionauth/auth_test.go @@ -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 {