mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-15 23:06:38 +00:00
185 lines
5.9 KiB
Go
185 lines
5.9 KiB
Go
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
|
|
}
|