mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
[client, management] Feature/ssh fine grained access (#4969)
Add fine-grained SSH access control with authorized users/groups
This commit is contained in:
184
client/ssh/auth/auth.go
Normal file
184
client/ssh/auth/auth.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
sshuserhash "github.com/netbirdio/netbird/shared/sshauth"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultUserIDClaim is the default JWT claim used to extract user IDs
|
||||
DefaultUserIDClaim = "sub"
|
||||
// Wildcard is a special user ID that matches all users
|
||||
Wildcard = "*"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrEmptyUserID = errors.New("JWT user ID is empty")
|
||||
ErrUserNotAuthorized = errors.New("user is not authorized to access this peer")
|
||||
ErrNoMachineUserMapping = errors.New("no authorization mapping for OS user")
|
||||
ErrUserNotMappedToOSUser = errors.New("user is not authorized to login as OS user")
|
||||
)
|
||||
|
||||
// Authorizer handles SSH fine-grained access control authorization
|
||||
type Authorizer struct {
|
||||
// UserIDClaim is the JWT claim to extract the user ID from
|
||||
userIDClaim string
|
||||
|
||||
// authorizedUsers is a list of hashed user IDs authorized to access this peer
|
||||
authorizedUsers []sshuserhash.UserIDHash
|
||||
|
||||
// machineUsers maps OS login usernames to lists of authorized user indexes
|
||||
machineUsers map[string][]uint32
|
||||
|
||||
// mu protects the list of users
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Config contains configuration for the SSH authorizer
|
||||
type Config struct {
|
||||
// UserIDClaim is the JWT claim to extract the user ID from (e.g., "sub", "email")
|
||||
UserIDClaim string
|
||||
|
||||
// AuthorizedUsers is a list of hashed user IDs (FNV-1a 64-bit) authorized to access this peer
|
||||
AuthorizedUsers []sshuserhash.UserIDHash
|
||||
|
||||
// MachineUsers maps OS login usernames to indexes in AuthorizedUsers
|
||||
// If a user wants to login as a specific OS user, their index must be in the corresponding list
|
||||
MachineUsers map[string][]uint32
|
||||
}
|
||||
|
||||
// NewAuthorizer creates a new SSH authorizer with empty configuration
|
||||
func NewAuthorizer() *Authorizer {
|
||||
a := &Authorizer{
|
||||
userIDClaim: DefaultUserIDClaim,
|
||||
machineUsers: make(map[string][]uint32),
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Update updates the authorizer configuration with new values
|
||||
func (a *Authorizer) Update(config *Config) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if config == nil {
|
||||
// Clear authorization
|
||||
a.userIDClaim = DefaultUserIDClaim
|
||||
a.authorizedUsers = []sshuserhash.UserIDHash{}
|
||||
a.machineUsers = make(map[string][]uint32)
|
||||
log.Info("SSH authorization cleared")
|
||||
return
|
||||
}
|
||||
|
||||
userIDClaim := config.UserIDClaim
|
||||
if userIDClaim == "" {
|
||||
userIDClaim = DefaultUserIDClaim
|
||||
}
|
||||
a.userIDClaim = userIDClaim
|
||||
|
||||
// Store authorized users list
|
||||
a.authorizedUsers = config.AuthorizedUsers
|
||||
|
||||
// Store machine users mapping
|
||||
machineUsers := make(map[string][]uint32)
|
||||
for osUser, indexes := range config.MachineUsers {
|
||||
if len(indexes) > 0 {
|
||||
machineUsers[osUser] = indexes
|
||||
}
|
||||
}
|
||||
a.machineUsers = machineUsers
|
||||
|
||||
log.Debugf("SSH auth: updated with %d authorized users, %d machine user mappings",
|
||||
len(config.AuthorizedUsers), len(machineUsers))
|
||||
}
|
||||
|
||||
// Authorize validates if a user is authorized to login as the specified OS user
|
||||
// Returns nil if authorized, or an error describing why authorization failed
|
||||
func (a *Authorizer) Authorize(jwtUserID, osUsername string) error {
|
||||
if jwtUserID == "" {
|
||||
log.Warnf("SSH auth denied: JWT user ID is empty for OS user '%s'", osUsername)
|
||||
return ErrEmptyUserID
|
||||
}
|
||||
|
||||
// Hash the JWT user ID for comparison
|
||||
hashedUserID, err := sshuserhash.HashUserID(jwtUserID)
|
||||
if err != nil {
|
||||
log.Errorf("SSH auth denied: failed to hash user ID '%s' for OS user '%s': %v", jwtUserID, osUsername, err)
|
||||
return fmt.Errorf("failed to hash user ID: %w", err)
|
||||
}
|
||||
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
|
||||
// Find the index of this user in the authorized list
|
||||
userIndex, found := a.findUserIndex(hashedUserID)
|
||||
if !found {
|
||||
log.Warnf("SSH auth denied: user '%s' (hash: %s) not in authorized list for OS user '%s'", jwtUserID, hashedUserID, osUsername)
|
||||
return ErrUserNotAuthorized
|
||||
}
|
||||
|
||||
return a.checkMachineUserMapping(jwtUserID, osUsername, userIndex)
|
||||
}
|
||||
|
||||
// checkMachineUserMapping validates if a user's index is authorized for the specified OS user
|
||||
// Checks wildcard mapping first, then specific OS user mappings
|
||||
func (a *Authorizer) checkMachineUserMapping(jwtUserID, osUsername string, userIndex int) error {
|
||||
// If wildcard exists and user's index is in the wildcard list, allow access to any OS user
|
||||
if wildcardIndexes, hasWildcard := a.machineUsers[Wildcard]; hasWildcard {
|
||||
if a.isIndexInList(uint32(userIndex), wildcardIndexes) {
|
||||
log.Infof("SSH auth granted: user '%s' authorized for OS user '%s' via wildcard (index: %d)", jwtUserID, osUsername, userIndex)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check for specific OS username mapping
|
||||
allowedIndexes, hasMachineUserMapping := a.machineUsers[osUsername]
|
||||
if !hasMachineUserMapping {
|
||||
// No mapping for this OS user - deny by default (fail closed)
|
||||
log.Warnf("SSH auth denied: no machine user mapping for OS user '%s' (JWT user: %s)", osUsername, jwtUserID)
|
||||
return ErrNoMachineUserMapping
|
||||
}
|
||||
|
||||
// Check if user's index is in the allowed indexes for this specific OS user
|
||||
if !a.isIndexInList(uint32(userIndex), allowedIndexes) {
|
||||
log.Warnf("SSH auth denied: user '%s' not mapped to OS user '%s' (user index: %d)", jwtUserID, osUsername, userIndex)
|
||||
return ErrUserNotMappedToOSUser
|
||||
}
|
||||
|
||||
log.Infof("SSH auth granted: user '%s' authorized for OS user '%s' (index: %d)", jwtUserID, osUsername, userIndex)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserIDClaim returns the JWT claim name used to extract user IDs
|
||||
func (a *Authorizer) GetUserIDClaim() string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.userIDClaim
|
||||
}
|
||||
|
||||
// findUserIndex finds the index of a hashed user ID in the authorized users list
|
||||
// Returns the index and true if found, 0 and false if not found
|
||||
func (a *Authorizer) findUserIndex(hashedUserID sshuserhash.UserIDHash) (int, bool) {
|
||||
for i, id := range a.authorizedUsers {
|
||||
if id == hashedUserID {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// isIndexInList checks if an index exists in a list of indexes
|
||||
func (a *Authorizer) isIndexInList(index uint32, indexes []uint32) bool {
|
||||
for _, idx := range indexes {
|
||||
if idx == index {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
612
client/ssh/auth/auth_test.go
Normal file
612
client/ssh/auth/auth_test.go
Normal file
@@ -0,0 +1,612 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/sshauth"
|
||||
)
|
||||
|
||||
func TestAuthorizer_Authorize_UserNotInList(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Set up authorized users list with one user
|
||||
authorizedUserHash, err := sshauth.HashUserID("authorized-user")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{authorizedUserHash},
|
||||
MachineUsers: map[string][]uint32{},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// Try to authorize a different user
|
||||
err = authorizer.Authorize("unauthorized-user", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotAuthorized)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Authorize_UserInList_NoMachineUserRestrictions(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
user2Hash, err := sshauth.HashUserID("user2")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash, user2Hash},
|
||||
MachineUsers: map[string][]uint32{}, // Empty = deny all (fail closed)
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// All attempts should fail when no machine user mappings exist (fail closed)
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
|
||||
err = authorizer.Authorize("user2", "admin")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
|
||||
err = authorizer.Authorize("user1", "postgres")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Authorize_UserInList_WithMachineUserMapping_Allowed(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
user2Hash, err := sshauth.HashUserID("user2")
|
||||
require.NoError(t, err)
|
||||
user3Hash, err := sshauth.HashUserID("user3")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash, user2Hash, user3Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": {0, 1}, // user1 and user2 can access root
|
||||
"postgres": {1, 2}, // user2 and user3 can access postgres
|
||||
"admin": {0}, // only user1 can access admin
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// user1 (index 0) should access root and admin
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user1", "admin")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// user2 (index 1) should access root and postgres
|
||||
err = authorizer.Authorize("user2", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user2", "postgres")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// user3 (index 2) should access postgres
|
||||
err = authorizer.Authorize("user3", "postgres")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Authorize_UserInList_WithMachineUserMapping_Denied(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Set up authorized users list
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
user2Hash, err := sshauth.HashUserID("user2")
|
||||
require.NoError(t, err)
|
||||
user3Hash, err := sshauth.HashUserID("user3")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash, user2Hash, user3Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": {0, 1}, // user1 and user2 can access root
|
||||
"postgres": {1, 2}, // user2 and user3 can access postgres
|
||||
"admin": {0}, // only user1 can access admin
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// user1 (index 0) should NOT access postgres
|
||||
err = authorizer.Authorize("user1", "postgres")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotMappedToOSUser)
|
||||
|
||||
// user2 (index 1) should NOT access admin
|
||||
err = authorizer.Authorize("user2", "admin")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotMappedToOSUser)
|
||||
|
||||
// user3 (index 2) should NOT access root
|
||||
err = authorizer.Authorize("user3", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotMappedToOSUser)
|
||||
|
||||
// user3 (index 2) should NOT access admin
|
||||
err = authorizer.Authorize("user3", "admin")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotMappedToOSUser)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Authorize_UserInList_OSUserNotInMapping(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Set up authorized users list
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": {0}, // only root is mapped
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// user1 should NOT access an unmapped OS user (fail closed)
|
||||
err = authorizer.Authorize("user1", "postgres")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Authorize_EmptyJWTUserID(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Set up authorized users list
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash},
|
||||
MachineUsers: map[string][]uint32{},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// Empty user ID should fail
|
||||
err = authorizer.Authorize("", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrEmptyUserID)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Authorize_MultipleUsersInList(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Set up multiple authorized users
|
||||
userHashes := make([]sshauth.UserIDHash, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
hash, err := sshauth.HashUserID("user" + string(rune('0'+i)))
|
||||
require.NoError(t, err)
|
||||
userHashes[i] = hash
|
||||
}
|
||||
|
||||
// Create machine user mapping for all users
|
||||
rootIndexes := make([]uint32, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
rootIndexes[i] = uint32(i)
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: userHashes,
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": rootIndexes,
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// All users should be authorized for root
|
||||
for i := 0; i < 10; i++ {
|
||||
err := authorizer.Authorize("user"+string(rune('0'+i)), "root")
|
||||
assert.NoError(t, err, "user%d should be authorized", i)
|
||||
}
|
||||
|
||||
// User not in list should fail
|
||||
err := authorizer.Authorize("unknown-user", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotAuthorized)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Update_ClearsConfiguration(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Set up initial configuration
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash},
|
||||
MachineUsers: map[string][]uint32{"root": {0}},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// user1 should be authorized
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Clear configuration
|
||||
authorizer.Update(nil)
|
||||
|
||||
// user1 should no longer be authorized
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotAuthorized)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Update_EmptyMachineUsersListEntries(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Machine users with empty index lists should be filtered out
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": {0},
|
||||
"postgres": {}, // empty list - should be filtered out
|
||||
"admin": nil, // nil list - should be filtered out
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// root should work
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// postgres should fail (no mapping)
|
||||
err = authorizer.Authorize("user1", "postgres")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
|
||||
// admin should fail (no mapping)
|
||||
err = authorizer.Authorize("user1", "admin")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
}
|
||||
|
||||
func TestAuthorizer_CustomUserIDClaim(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Set up with custom user ID claim
|
||||
user1Hash, err := sshauth.HashUserID("user@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: "email",
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": {0},
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// Verify the custom claim is set
|
||||
assert.Equal(t, "email", authorizer.GetUserIDClaim())
|
||||
|
||||
// Authorize with email as user ID
|
||||
err = authorizer.Authorize("user@example.com", "root")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthorizer_DefaultUserIDClaim(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Verify default claim
|
||||
assert.Equal(t, DefaultUserIDClaim, authorizer.GetUserIDClaim())
|
||||
assert.Equal(t, "sub", authorizer.GetUserIDClaim())
|
||||
|
||||
// Set up with empty user ID claim (should use default)
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: "", // empty - should use default
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash},
|
||||
MachineUsers: map[string][]uint32{},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// Should fall back to default
|
||||
assert.Equal(t, DefaultUserIDClaim, authorizer.GetUserIDClaim())
|
||||
}
|
||||
|
||||
func TestAuthorizer_MachineUserMapping_LargeIndexes(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Create a large authorized users list
|
||||
const numUsers = 1000
|
||||
userHashes := make([]sshauth.UserIDHash, numUsers)
|
||||
for i := 0; i < numUsers; i++ {
|
||||
hash, err := sshauth.HashUserID("user" + string(rune(i)))
|
||||
require.NoError(t, err)
|
||||
userHashes[i] = hash
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: userHashes,
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": {0, 500, 999}, // first, middle, and last user
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// First user should have access
|
||||
err := authorizer.Authorize("user"+string(rune(0)), "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Middle user should have access
|
||||
err = authorizer.Authorize("user"+string(rune(500)), "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Last user should have access
|
||||
err = authorizer.Authorize("user"+string(rune(999)), "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// User not in mapping should NOT have access
|
||||
err = authorizer.Authorize("user"+string(rune(100)), "root")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAuthorizer_ConcurrentAuthorization(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Set up authorized users
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
user2Hash, err := sshauth.HashUserID("user2")
|
||||
require.NoError(t, err)
|
||||
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash, user2Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": {0, 1},
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// Test concurrent authorization calls (should be safe to read concurrently)
|
||||
const numGoroutines = 100
|
||||
errChan := make(chan error, numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(idx int) {
|
||||
user := "user1"
|
||||
if idx%2 == 0 {
|
||||
user = "user2"
|
||||
}
|
||||
err := authorizer.Authorize(user, "root")
|
||||
errChan <- err
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete and collect errors
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
err := <-errChan
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizer_Wildcard_AllowsAllAuthorizedUsers(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
user2Hash, err := sshauth.HashUserID("user2")
|
||||
require.NoError(t, err)
|
||||
user3Hash, err := sshauth.HashUserID("user3")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Configure with wildcard - all authorized users can access any OS user
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash, user2Hash, user3Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"*": {0, 1, 2}, // wildcard with all user indexes
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// All authorized users should be able to access any OS user
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user2", "postgres")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user3", "admin")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user1", "ubuntu")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user2", "nginx")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user3", "docker")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Wildcard_UnauthorizedUserStillDenied(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Configure with wildcard
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"*": {0},
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// user1 should have access
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Unauthorized user should still be denied even with wildcard
|
||||
err = authorizer.Authorize("unauthorized-user", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotAuthorized)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Wildcard_TakesPrecedenceOverSpecificMappings(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
user2Hash, err := sshauth.HashUserID("user2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Configure with both wildcard and specific mappings
|
||||
// Wildcard takes precedence for users in the wildcard index list
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash, user2Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"*": {0, 1}, // wildcard for both users
|
||||
"root": {0}, // specific mapping that would normally restrict to user1 only
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// Both users should be able to access root via wildcard (takes precedence over specific mapping)
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user2", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Both users should be able to access any other OS user via wildcard
|
||||
err = authorizer.Authorize("user1", "postgres")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = authorizer.Authorize("user2", "admin")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthorizer_NoWildcard_SpecificMappingsOnly(t *testing.T) {
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
user1Hash, err := sshauth.HashUserID("user1")
|
||||
require.NoError(t, err)
|
||||
user2Hash, err := sshauth.HashUserID("user2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Configure WITHOUT wildcard - only specific mappings
|
||||
config := &Config{
|
||||
UserIDClaim: DefaultUserIDClaim,
|
||||
AuthorizedUsers: []sshauth.UserIDHash{user1Hash, user2Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"root": {0}, // only user1
|
||||
"postgres": {1}, // only user2
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// user1 can access root
|
||||
err = authorizer.Authorize("user1", "root")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// user2 can access postgres
|
||||
err = authorizer.Authorize("user2", "postgres")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// user1 cannot access postgres
|
||||
err = authorizer.Authorize("user1", "postgres")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotMappedToOSUser)
|
||||
|
||||
// user2 cannot access root
|
||||
err = authorizer.Authorize("user2", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotMappedToOSUser)
|
||||
|
||||
// Neither can access unmapped OS users
|
||||
err = authorizer.Authorize("user1", "admin")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
|
||||
err = authorizer.Authorize("user2", "admin")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
}
|
||||
|
||||
func TestAuthorizer_Wildcard_WithPartialIndexes_AllowsAllUsers(t *testing.T) {
|
||||
// This test covers the scenario where wildcard exists with limited indexes.
|
||||
// Only users whose indexes are in the wildcard list can access any OS user via wildcard.
|
||||
// Other users can only access OS users they are explicitly mapped to.
|
||||
authorizer := NewAuthorizer()
|
||||
|
||||
// Create two authorized user hashes (simulating the base64-encoded hashes in the config)
|
||||
wasmHash, err := sshauth.HashUserID("wasm")
|
||||
require.NoError(t, err)
|
||||
user2Hash, err := sshauth.HashUserID("user2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Configure with wildcard having only index 0, and specific mappings for other OS users
|
||||
config := &Config{
|
||||
UserIDClaim: "sub",
|
||||
AuthorizedUsers: []sshauth.UserIDHash{wasmHash, user2Hash},
|
||||
MachineUsers: map[string][]uint32{
|
||||
"*": {0}, // wildcard with only index 0 - only wasm has wildcard access
|
||||
"alice": {1}, // specific mapping for user2
|
||||
"bob": {1}, // specific mapping for user2
|
||||
},
|
||||
}
|
||||
authorizer.Update(config)
|
||||
|
||||
// wasm (index 0) should access any OS user via wildcard
|
||||
err = authorizer.Authorize("wasm", "root")
|
||||
assert.NoError(t, err, "wasm should access root via wildcard")
|
||||
|
||||
err = authorizer.Authorize("wasm", "alice")
|
||||
assert.NoError(t, err, "wasm should access alice via wildcard")
|
||||
|
||||
err = authorizer.Authorize("wasm", "bob")
|
||||
assert.NoError(t, err, "wasm should access bob via wildcard")
|
||||
|
||||
err = authorizer.Authorize("wasm", "postgres")
|
||||
assert.NoError(t, err, "wasm should access postgres via wildcard")
|
||||
|
||||
// user2 (index 1) should only access alice and bob (explicitly mapped), NOT root or postgres
|
||||
err = authorizer.Authorize("user2", "alice")
|
||||
assert.NoError(t, err, "user2 should access alice via explicit mapping")
|
||||
|
||||
err = authorizer.Authorize("user2", "bob")
|
||||
assert.NoError(t, err, "user2 should access bob via explicit mapping")
|
||||
|
||||
err = authorizer.Authorize("user2", "root")
|
||||
assert.Error(t, err, "user2 should NOT access root (not in wildcard indexes)")
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
|
||||
err = authorizer.Authorize("user2", "postgres")
|
||||
assert.Error(t, err, "user2 should NOT access postgres (not explicitly mapped)")
|
||||
assert.ErrorIs(t, err, ErrNoMachineUserMapping)
|
||||
|
||||
// Unauthorized user should still be denied
|
||||
err = authorizer.Authorize("user3", "root")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrUserNotAuthorized, "unauthorized user should be denied")
|
||||
}
|
||||
Reference in New Issue
Block a user