Files
netbird/client/ssh/server/user_utils_test.go
2025-11-17 17:10:41 +01:00

909 lines
36 KiB
Go

package server
import (
"errors"
"os/user"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
// Test helper functions
func createTestUser(username, uid, gid, homeDir string) *user.User {
return &user.User{
Uid: uid,
Gid: gid,
Username: username,
Name: username,
HomeDir: homeDir,
}
}
// Test dependency injection setup - injects platform dependencies to test real logic
func setupTestDependencies(currentUser *user.User, currentUserErr error, os string, euid int, lookupUsers map[string]*user.User, lookupErrors map[string]error) func() {
// Store originals
originalGetCurrentUser := getCurrentUser
originalLookupUser := lookupUser
originalGetCurrentOS := getCurrentOS
originalGetEuid := getEuid
// Reset caches to ensure clean test state
// Set test values - inject platform dependencies
getCurrentUser = func() (*user.User, error) {
return currentUser, currentUserErr
}
lookupUser = func(username string) (*user.User, error) {
if err, exists := lookupErrors[username]; exists {
return nil, err
}
if userObj, exists := lookupUsers[username]; exists {
return userObj, nil
}
return nil, errors.New("user: unknown user " + username)
}
getCurrentOS = func() string {
return os
}
getEuid = func() int {
return euid
}
// Mock privilege detection based on the test user
getIsProcessPrivileged = func() bool {
if currentUser == nil {
return false
}
// Check both username and SID for Windows systems
if os == "windows" && isWindowsPrivilegedSID(currentUser.Uid) {
return true
}
return isPrivilegedUsername(currentUser.Username)
}
// Return cleanup function
return func() {
getCurrentUser = originalGetCurrentUser
lookupUser = originalLookupUser
getCurrentOS = originalGetCurrentOS
getEuid = originalGetEuid
getIsProcessPrivileged = isCurrentProcessPrivileged
// Reset caches after test
}
}
func TestCheckPrivileges_ComprehensiveMatrix(t *testing.T) {
tests := []struct {
name string
os string
euid int
currentUser *user.User
requestedUsername string
featureSupportsUserSwitch bool
allowRoot bool
lookupUsers map[string]*user.User
expectedAllowed bool
expectedRequiresSwitch bool
}{
{
name: "linux_root_can_switch_to_alice",
os: "linux",
euid: 0, // Root process
currentUser: createTestUser("root", "0", "0", "/root"),
requestedUsername: "alice",
featureSupportsUserSwitch: true,
allowRoot: true,
lookupUsers: map[string]*user.User{
"alice": createTestUser("alice", "1000", "1000", "/home/alice"),
},
expectedAllowed: true,
expectedRequiresSwitch: true,
},
{
name: "linux_non_root_fallback_to_current_user",
os: "linux",
euid: 1000, // Non-root process
currentUser: createTestUser("alice", "1000", "1000", "/home/alice"),
requestedUsername: "bob",
featureSupportsUserSwitch: true,
allowRoot: true,
expectedAllowed: true, // Should fallback to current user (alice)
expectedRequiresSwitch: false, // Fallback means no actual switching
},
{
name: "windows_admin_can_switch_to_alice",
os: "windows",
euid: 1000, // Irrelevant on Windows
currentUser: createTestUser("Administrator", "S-1-5-21-123456789-123456789-123456789-500", "S-1-5-32-544", "C:\\Users\\Administrator"),
requestedUsername: "alice",
featureSupportsUserSwitch: true,
allowRoot: true,
lookupUsers: map[string]*user.User{
"alice": createTestUser("alice", "S-1-5-21-123456789-123456789-123456789-1001", "S-1-5-21-123456789-123456789-123456789-513", "C:\\Users\\alice"),
},
expectedAllowed: true,
expectedRequiresSwitch: true,
},
{
name: "windows_non_admin_no_fallback_hard_failure",
os: "windows",
euid: 1000, // Irrelevant on Windows
currentUser: createTestUser("alice", "1001", "1001", "C:\\Users\\alice"),
requestedUsername: "bob",
featureSupportsUserSwitch: true,
allowRoot: true,
lookupUsers: map[string]*user.User{
"bob": createTestUser("bob", "S-1-5-21-123456789-123456789-123456789-1002", "S-1-5-21-123456789-123456789-123456789-513", "C:\\Users\\bob"),
},
expectedAllowed: true, // Let OS decide - deferred security check
expectedRequiresSwitch: true, // Different user was requested
},
// Comprehensive test matrix: non-root linux with different allowRoot settings
{
name: "linux_non_root_request_root_allowRoot_false",
os: "linux",
euid: 1000,
currentUser: createTestUser("alice", "1000", "1000", "/home/alice"),
requestedUsername: "root",
featureSupportsUserSwitch: true,
allowRoot: false,
expectedAllowed: true, // Fallback allows access regardless of root setting
expectedRequiresSwitch: false, // Fallback case, no switching
},
{
name: "linux_non_root_request_root_allowRoot_true",
os: "linux",
euid: 1000,
currentUser: createTestUser("alice", "1000", "1000", "/home/alice"),
requestedUsername: "root",
featureSupportsUserSwitch: true,
allowRoot: true,
expectedAllowed: true, // Should fallback to alice (non-privileged process)
expectedRequiresSwitch: false, // Fallback means no actual switching
},
// Windows admin test matrix
{
name: "windows_admin_request_root_allowRoot_false",
os: "windows",
euid: 1000,
currentUser: createTestUser("Administrator", "S-1-5-21-123456789-123456789-123456789-500", "S-1-5-32-544", "C:\\Users\\Administrator"),
requestedUsername: "root",
featureSupportsUserSwitch: true,
allowRoot: false,
expectedAllowed: false, // Root not allowed
expectedRequiresSwitch: true,
},
{
name: "windows_admin_request_root_allowRoot_true",
os: "windows",
euid: 1000,
currentUser: createTestUser("Administrator", "S-1-5-21-123456789-123456789-123456789-500", "S-1-5-32-544", "C:\\Users\\Administrator"),
requestedUsername: "root",
featureSupportsUserSwitch: true,
allowRoot: true,
lookupUsers: map[string]*user.User{
"root": createTestUser("root", "0", "0", "/root"),
},
expectedAllowed: true, // Windows user switching should work like Unix
expectedRequiresSwitch: true,
},
// Windows non-admin test matrix
{
name: "windows_non_admin_request_root_allowRoot_false",
os: "windows",
euid: 1000,
currentUser: createTestUser("alice", "S-1-5-21-123456789-123456789-123456789-1001", "S-1-5-21-123456789-123456789-123456789-513", "C:\\Users\\alice"),
requestedUsername: "root",
featureSupportsUserSwitch: true,
allowRoot: false,
expectedAllowed: false, // Root not allowed (allowRoot=false takes precedence)
expectedRequiresSwitch: true,
},
{
name: "windows_system_account_allowRoot_false",
os: "windows",
euid: 1000,
currentUser: createTestUser("NETBIRD\\WIN2K19-C2$", "S-1-5-18", "S-1-5-18", "C:\\Windows\\System32"),
requestedUsername: "root",
featureSupportsUserSwitch: true,
allowRoot: false,
expectedAllowed: false, // Root not allowed
expectedRequiresSwitch: true,
},
{
name: "windows_system_account_allowRoot_true",
os: "windows",
euid: 1000,
currentUser: createTestUser("NETBIRD\\WIN2K19-C2$", "S-1-5-18", "S-1-5-18", "C:\\Windows\\System32"),
requestedUsername: "root",
featureSupportsUserSwitch: true,
allowRoot: true,
lookupUsers: map[string]*user.User{
"root": createTestUser("root", "0", "0", "/root"),
},
expectedAllowed: true, // SYSTEM can switch to root
expectedRequiresSwitch: true,
},
{
name: "windows_non_admin_request_root_allowRoot_true",
os: "windows",
euid: 1000,
currentUser: createTestUser("alice", "S-1-5-21-123456789-123456789-123456789-1001", "S-1-5-21-123456789-123456789-123456789-513", "C:\\Users\\alice"),
requestedUsername: "root",
featureSupportsUserSwitch: true,
allowRoot: true,
lookupUsers: map[string]*user.User{
"root": createTestUser("root", "0", "0", "/root"),
},
expectedAllowed: true, // Let OS decide - deferred security check
expectedRequiresSwitch: true,
},
// Feature doesn't support user switching scenarios
{
name: "linux_root_feature_no_user_switching_same_user",
os: "linux",
euid: 0,
currentUser: createTestUser("root", "0", "0", "/root"),
requestedUsername: "root", // Same user
featureSupportsUserSwitch: false,
allowRoot: true,
lookupUsers: map[string]*user.User{
"root": createTestUser("root", "0", "0", "/root"),
},
expectedAllowed: true, // Same user should work regardless of feature support
expectedRequiresSwitch: false,
},
{
name: "linux_root_feature_no_user_switching_different_user",
os: "linux",
euid: 0,
currentUser: createTestUser("root", "0", "0", "/root"),
requestedUsername: "alice",
featureSupportsUserSwitch: false, // Feature doesn't support switching
allowRoot: true,
lookupUsers: map[string]*user.User{
"alice": createTestUser("alice", "1000", "1000", "/home/alice"),
},
expectedAllowed: false, // Should deny because feature doesn't support switching
expectedRequiresSwitch: true,
},
// Empty username (current user) scenarios
{
name: "linux_non_root_current_user_empty_username",
os: "linux",
euid: 1000,
currentUser: createTestUser("alice", "1000", "1000", "/home/alice"),
requestedUsername: "", // Empty = current user
featureSupportsUserSwitch: true,
allowRoot: false,
expectedAllowed: true, // Current user should always work
expectedRequiresSwitch: false,
},
{
name: "linux_root_current_user_empty_username_root_not_allowed",
os: "linux",
euid: 0,
currentUser: createTestUser("root", "0", "0", "/root"),
requestedUsername: "", // Empty = current user (root)
featureSupportsUserSwitch: true,
allowRoot: false, // Root not allowed
expectedAllowed: false, // Should deny root even when it's current user
expectedRequiresSwitch: false,
},
// User not found scenarios
{
name: "linux_root_user_not_found",
os: "linux",
euid: 0,
currentUser: createTestUser("root", "0", "0", "/root"),
requestedUsername: "nonexistent",
featureSupportsUserSwitch: true,
allowRoot: true,
lookupUsers: map[string]*user.User{}, // No users defined = user not found
expectedAllowed: false, // Should fail due to user not found
expectedRequiresSwitch: true,
},
// Windows feature doesn't support user switching
{
name: "windows_admin_feature_no_user_switching_different_user",
os: "windows",
euid: 1000,
currentUser: createTestUser("Administrator", "S-1-5-21-123456789-123456789-123456789-500", "S-1-5-32-544", "C:\\Users\\Administrator"),
requestedUsername: "alice",
featureSupportsUserSwitch: false, // Feature doesn't support switching
allowRoot: true,
lookupUsers: map[string]*user.User{
"alice": createTestUser("alice", "S-1-5-21-123456789-123456789-123456789-1001", "S-1-5-21-123456789-123456789-123456789-513", "C:\\Users\\alice"),
},
expectedAllowed: false, // Should deny because feature doesn't support switching
expectedRequiresSwitch: true,
},
// Windows regular user scenarios (non-admin)
{
name: "windows_regular_user_same_user",
os: "windows",
euid: 1000,
currentUser: createTestUser("alice", "S-1-5-21-123456789-123456789-123456789-1001", "S-1-5-21-123456789-123456789-123456789-513", "C:\\Users\\alice"),
requestedUsername: "alice", // Same user
featureSupportsUserSwitch: true,
allowRoot: false,
lookupUsers: map[string]*user.User{
"alice": createTestUser("alice", "S-1-5-21-123456789-123456789-123456789-1001", "S-1-5-21-123456789-123456789-123456789-513", "C:\\Users\\alice"),
},
expectedAllowed: true, // Regular user accessing themselves should work
expectedRequiresSwitch: false, // No switching for same user
},
{
name: "windows_regular_user_empty_username",
os: "windows",
euid: 1000,
currentUser: createTestUser("alice", "S-1-5-21-123456789-123456789-123456789-1001", "S-1-5-21-123456789-123456789-123456789-513", "C:\\Users\\alice"),
requestedUsername: "", // Empty = current user
featureSupportsUserSwitch: true,
allowRoot: false,
expectedAllowed: true, // Current user should always work
expectedRequiresSwitch: false, // No switching for current user
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Inject platform dependencies to test real logic
cleanup := setupTestDependencies(tt.currentUser, nil, tt.os, tt.euid, tt.lookupUsers, nil)
defer cleanup()
server := &Server{allowRootLogin: tt.allowRoot}
result := server.CheckPrivileges(PrivilegeCheckRequest{
RequestedUsername: tt.requestedUsername,
FeatureSupportsUserSwitch: tt.featureSupportsUserSwitch,
FeatureName: "SSH login",
})
assert.Equal(t, tt.expectedAllowed, result.Allowed)
assert.Equal(t, tt.expectedRequiresSwitch, result.RequiresUserSwitching)
})
}
}
func TestUsedFallback_MeansNoPrivilegeDropping(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Fallback mechanism is Unix-specific")
}
// Create test scenario where fallback should occur
server := &Server{allowRootLogin: true}
// Mock dependencies to simulate non-privileged user
originalGetCurrentUser := getCurrentUser
originalGetIsProcessPrivileged := getIsProcessPrivileged
defer func() {
getCurrentUser = originalGetCurrentUser
getIsProcessPrivileged = originalGetIsProcessPrivileged
}()
// Set up mocks for fallback scenario
getCurrentUser = func() (*user.User, error) {
return createTestUser("netbird", "1000", "1000", "/var/lib/netbird"), nil
}
getIsProcessPrivileged = func() bool { return false } // Non-privileged
// Request different user - should fallback
result := server.CheckPrivileges(PrivilegeCheckRequest{
RequestedUsername: "alice",
FeatureSupportsUserSwitch: true,
FeatureName: "SSH login",
})
// Verify fallback occurred
assert.True(t, result.Allowed, "Should allow with fallback")
assert.True(t, result.UsedFallback, "Should indicate fallback was used")
assert.Equal(t, "netbird", result.User.Username, "Should return current user")
assert.False(t, result.RequiresUserSwitching, "Should not require switching when fallback is used")
// Key assertion: When UsedFallback is true, no privilege dropping should be needed
// because all privilege checks have already been performed and we're using current user
t.Logf("UsedFallback=true means: current user (%s) is the target, no privilege dropping needed",
result.User.Username)
}
func TestPrivilegedUsernameDetection(t *testing.T) {
tests := []struct {
name string
username string
platform string
privileged bool
}{
// Unix/Linux tests
{"unix_root", "root", "linux", true},
{"unix_regular_user", "alice", "linux", false},
{"unix_root_capital", "Root", "linux", false}, // Case-sensitive
// Windows tests
{"windows_administrator", "Administrator", "windows", true},
{"windows_system", "SYSTEM", "windows", true},
{"windows_admin", "admin", "windows", true},
{"windows_admin_lowercase", "administrator", "windows", true}, // Case-insensitive
{"windows_domain_admin", "DOMAIN\\Administrator", "windows", true},
{"windows_email_admin", "admin@domain.com", "windows", true},
{"windows_regular_user", "alice", "windows", false},
{"windows_domain_user", "DOMAIN\\alice", "windows", false},
{"windows_localsystem", "localsystem", "windows", true},
{"windows_networkservice", "networkservice", "windows", true},
{"windows_localservice", "localservice", "windows", true},
// Computer accounts (these depend on current user context in real implementation)
{"windows_computer_account", "WIN2K19-C2$", "windows", false}, // Computer account by itself not privileged
{"windows_domain_computer", "DOMAIN\\COMPUTER$", "windows", false}, // Domain computer account
// Cross-platform
{"root_on_windows", "root", "windows", true}, // Root should be privileged everywhere
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mock the platform for this test
cleanup := setupTestDependencies(nil, nil, tt.platform, 1000, nil, nil)
defer cleanup()
result := isPrivilegedUsername(tt.username)
assert.Equal(t, tt.privileged, result)
})
}
}
func TestWindowsPrivilegedSIDDetection(t *testing.T) {
tests := []struct {
name string
sid string
privileged bool
description string
}{
// Well-known system accounts
{"system_account", "S-1-5-18", true, "Local System (SYSTEM)"},
{"local_service", "S-1-5-19", true, "Local Service"},
{"network_service", "S-1-5-20", true, "Network Service"},
{"administrators_group", "S-1-5-32-544", true, "Administrators group"},
{"builtin_administrator", "S-1-5-500", true, "Built-in Administrator"},
// Domain accounts
{"domain_administrator", "S-1-5-21-1234567890-1234567890-1234567890-500", true, "Domain Administrator (RID 500)"},
{"domain_admins_group", "S-1-5-21-1234567890-1234567890-1234567890-512", true, "Domain Admins group"},
{"domain_controllers_group", "S-1-5-21-1234567890-1234567890-1234567890-516", true, "Domain Controllers group"},
{"enterprise_admins_group", "S-1-5-21-1234567890-1234567890-1234567890-519", true, "Enterprise Admins group"},
// Regular users
{"regular_user", "S-1-5-21-1234567890-1234567890-1234567890-1001", false, "Regular domain user"},
{"another_regular_user", "S-1-5-21-1234567890-1234567890-1234567890-1234", false, "Another regular user"},
{"local_user", "S-1-5-21-1234567890-1234567890-1234567890-1000", false, "Local regular user"},
// Groups that are not privileged
{"domain_users", "S-1-5-21-1234567890-1234567890-1234567890-513", false, "Domain Users group"},
{"power_users", "S-1-5-32-547", false, "Power Users group"},
// Invalid SIDs
{"malformed_sid", "S-1-5-invalid", false, "Malformed SID"},
{"empty_sid", "", false, "Empty SID"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isWindowsPrivilegedSID(tt.sid)
assert.Equal(t, tt.privileged, result, "Failed for %s: %s", tt.description, tt.sid)
})
}
}
func TestIsSameUser(t *testing.T) {
tests := []struct {
name string
user1 string
user2 string
os string
expected bool
}{
// Basic cases
{"same_username", "alice", "alice", "linux", true},
{"different_username", "alice", "bob", "linux", false},
// Linux (no domain processing)
{"linux_domain_vs_bare", "DOMAIN\\alice", "alice", "linux", false},
{"linux_email_vs_bare", "alice@domain.com", "alice", "linux", false},
{"linux_same_literal", "DOMAIN\\alice", "DOMAIN\\alice", "linux", true},
// Windows (with domain processing) - Note: parameter order is (requested, current, os, expected)
{"windows_domain_vs_bare", "alice", "DOMAIN\\alice", "windows", true}, // bare username matches domain current user
{"windows_email_vs_bare", "alice", "alice@domain.com", "windows", true}, // bare username matches email current user
{"windows_different_domains_same_user", "DOMAIN1\\alice", "DOMAIN2\\alice", "windows", false}, // SECURITY: different domains = different users
{"windows_case_insensitive", "Alice", "alice", "windows", true},
{"windows_different_users", "DOMAIN\\alice", "DOMAIN\\bob", "windows", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up OS mock
cleanup := setupTestDependencies(nil, nil, tt.os, 1000, nil, nil)
defer cleanup()
result := isSameUser(tt.user1, tt.user2)
assert.Equal(t, tt.expected, result)
})
}
}
func TestUsernameValidation_Unix(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Unix-specific username validation tests")
}
tests := []struct {
name string
username string
wantErr bool
errMsg string
}{
// Valid usernames (Unix/POSIX)
{"valid_alphanumeric", "user123", false, ""},
{"valid_with_dots", "user.name", false, ""},
{"valid_with_hyphens", "user-name", false, ""},
{"valid_with_underscores", "user_name", false, ""},
{"valid_uppercase", "UserName", false, ""},
{"valid_starting_with_digit", "123user", false, ""},
{"valid_starting_with_dot", ".hidden", false, ""},
// Invalid usernames (Unix/POSIX)
{"empty_username", "", true, "username cannot be empty"},
{"username_too_long", "thisusernameiswaytoolongandexceedsthe32characterlimit", true, "username too long"},
{"username_starting_with_hyphen", "-user", true, "invalid characters"}, // POSIX restriction
{"username_with_spaces", "user name", true, "invalid characters"},
{"username_with_shell_metacharacters", "user;rm", true, "invalid characters"},
{"username_with_command_injection", "user`rm -rf /`", true, "invalid characters"},
{"username_with_pipe", "user|rm", true, "invalid characters"},
{"username_with_ampersand", "user&rm", true, "invalid characters"},
{"username_with_quotes", "user\"name", true, "invalid characters"},
{"username_with_newline", "user\nname", true, "invalid characters"},
{"reserved_dot", ".", true, "cannot be '.' or '..'"},
{"reserved_dotdot", "..", true, "cannot be '.' or '..'"},
{"username_with_at_symbol", "user@domain", true, "invalid characters"}, // Not allowed in bare Unix usernames
{"username_with_backslash", "user\\name", true, "invalid characters"}, // Not allowed in Unix usernames
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateUsername(tt.username)
if tt.wantErr {
assert.Error(t, err, "Should reject invalid username")
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg, "Error message should contain expected text")
}
} else {
assert.NoError(t, err, "Should accept valid username")
}
})
}
}
func TestUsernameValidation_Windows(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows-specific username validation tests")
}
tests := []struct {
name string
username string
wantErr bool
errMsg string
}{
// Valid usernames (Windows)
{"valid_alphanumeric", "user123", false, ""},
{"valid_with_dots", "user.name", false, ""},
{"valid_with_hyphens", "user-name", false, ""},
{"valid_with_underscores", "user_name", false, ""},
{"valid_uppercase", "UserName", false, ""},
{"valid_starting_with_digit", "123user", false, ""},
{"valid_starting_with_dot", ".hidden", false, ""},
{"valid_starting_with_hyphen", "-user", false, ""}, // Windows allows this
{"valid_domain_username", "DOMAIN\\user", false, ""}, // Windows domain format
{"valid_email_username", "user@domain.com", false, ""}, // Windows email format
{"valid_machine_username", "MACHINE\\user", false, ""}, // Windows machine format
// Invalid usernames (Windows)
{"empty_username", "", true, "username cannot be empty"},
{"username_too_long", "thisusernameiswaytoolongandexceedsthe32characterlimit", true, "username too long"},
{"username_with_spaces", "user name", true, "invalid characters"},
{"username_with_shell_metacharacters", "user;rm", true, "invalid characters"},
{"username_with_command_injection", "user`rm -rf /`", true, "invalid characters"},
{"username_with_pipe", "user|rm", true, "invalid characters"},
{"username_with_ampersand", "user&rm", true, "invalid characters"},
{"username_with_quotes", "user\"name", true, "invalid characters"},
{"username_with_newline", "user\nname", true, "invalid characters"},
{"username_with_brackets", "user[name]", true, "invalid characters"},
{"username_with_colon", "user:name", true, "invalid characters"},
{"username_with_semicolon", "user;name", true, "invalid characters"},
{"username_with_equals", "user=name", true, "invalid characters"},
{"username_with_comma", "user,name", true, "invalid characters"},
{"username_with_plus", "user+name", true, "invalid characters"},
{"username_with_asterisk", "user*name", true, "invalid characters"},
{"username_with_question", "user?name", true, "invalid characters"},
{"username_with_angles", "user<name>", true, "invalid characters"},
{"reserved_dot", ".", true, "cannot be '.' or '..'"},
{"reserved_dotdot", "..", true, "cannot be '.' or '..'"},
{"username_ending_with_period", "user.", true, "cannot end with a period"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateUsername(tt.username)
if tt.wantErr {
assert.Error(t, err, "Should reject invalid username")
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg, "Error message should contain expected text")
}
} else {
assert.NoError(t, err, "Should accept valid username")
}
})
}
}
// Test real-world integration scenarios with actual platform capabilities
func TestCheckPrivileges_RealWorldScenarios(t *testing.T) {
tests := []struct {
name string
feature string
featureSupportsUserSwitch bool
requestedUsername string
allowRoot bool
expectedBehaviorPattern string
}{
{"SSH_login_current_user", "SSH login", true, "", true, "should_allow_current_user"},
{"SFTP_current_user", "SFTP", true, "", true, "should_allow_current_user"},
{"port_forwarding_current_user", "port forwarding", false, "", true, "should_allow_current_user"},
{"SSH_login_root_not_allowed", "SSH login", true, "root", false, "should_deny_root"},
{"port_forwarding_different_user", "port forwarding", false, "differentuser", true, "should_deny_switching"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mock privileged environment to ensure consistent test behavior across environments
cleanup := setupTestDependencies(
createTestUser("root", "0", "0", "/root"), // Running as root
nil,
runtime.GOOS,
0, // euid 0 (root)
map[string]*user.User{
"root": createTestUser("root", "0", "0", "/root"),
"differentuser": createTestUser("differentuser", "1000", "1000", "/home/differentuser"),
},
nil,
)
defer cleanup()
server := &Server{allowRootLogin: tt.allowRoot}
result := server.CheckPrivileges(PrivilegeCheckRequest{
RequestedUsername: tt.requestedUsername,
FeatureSupportsUserSwitch: tt.featureSupportsUserSwitch,
FeatureName: tt.feature,
})
switch tt.expectedBehaviorPattern {
case "should_allow_current_user":
assert.True(t, result.Allowed, "Should allow current user access")
assert.False(t, result.RequiresUserSwitching, "Current user should not require switching")
case "should_deny_root":
assert.False(t, result.Allowed, "Should deny root when not allowed")
assert.Contains(t, result.Error.Error(), "root", "Should mention root in error")
case "should_deny_switching":
assert.False(t, result.Allowed, "Should deny when feature doesn't support switching")
assert.Contains(t, result.Error.Error(), "user switching not supported", "Should mention switching in error")
}
})
}
}
// Test with actual platform capabilities - no mocking
func TestCheckPrivileges_ActualPlatform(t *testing.T) {
// This test uses the REAL platform capabilities
server := &Server{allowRootLogin: true}
// Test current user access - should always work
result := server.CheckPrivileges(PrivilegeCheckRequest{
RequestedUsername: "", // Current user
FeatureSupportsUserSwitch: true,
FeatureName: "SSH login",
})
assert.True(t, result.Allowed, "Current user should always be allowed")
assert.False(t, result.RequiresUserSwitching, "Current user should not require switching")
assert.NotNil(t, result.User, "Should return current user")
// Test user switching capability based on actual platform
actualIsPrivileged := isCurrentProcessPrivileged() // REAL check
actualOS := runtime.GOOS // REAL check
t.Logf("Platform capabilities: OS=%s, isPrivileged=%v, supportsUserSwitching=%v",
actualOS, actualIsPrivileged, actualIsPrivileged)
// Test requesting different user
result = server.CheckPrivileges(PrivilegeCheckRequest{
RequestedUsername: "nonexistentuser",
FeatureSupportsUserSwitch: true,
FeatureName: "SSH login",
})
switch {
case actualOS == "windows":
// Windows supports user switching but should fail on nonexistent user
assert.False(t, result.Allowed, "Windows should deny nonexistent user")
assert.True(t, result.RequiresUserSwitching, "Should indicate switching is needed")
assert.Contains(t, result.Error.Error(), "not found",
"Should indicate user not found")
case !actualIsPrivileged:
// Non-privileged Unix processes should fallback to current user
assert.True(t, result.Allowed, "Non-privileged Unix process should fallback to current user")
assert.False(t, result.RequiresUserSwitching, "Fallback means no switching actually happens")
assert.True(t, result.UsedFallback, "Should indicate fallback was used")
assert.NotNil(t, result.User, "Should return current user")
default:
// Privileged Unix processes should attempt user lookup
assert.False(t, result.Allowed, "Should fail due to nonexistent user")
assert.True(t, result.RequiresUserSwitching, "Should indicate switching is needed")
assert.Contains(t, result.Error.Error(), "nonexistentuser",
"Should indicate user not found")
}
}
// Test platform detection logic with dependency injection
func TestPlatformLogic_DependencyInjection(t *testing.T) {
tests := []struct {
name string
os string
euid int
currentUser *user.User
expectedIsProcessPrivileged bool
expectedSupportsUserSwitching bool
}{
{
name: "linux_root_process",
os: "linux",
euid: 0,
currentUser: createTestUser("root", "0", "0", "/root"),
expectedIsProcessPrivileged: true,
expectedSupportsUserSwitching: true,
},
{
name: "linux_non_root_process",
os: "linux",
euid: 1000,
currentUser: createTestUser("alice", "1000", "1000", "/home/alice"),
expectedIsProcessPrivileged: false,
expectedSupportsUserSwitching: false,
},
{
name: "windows_admin_process",
os: "windows",
euid: 1000, // euid ignored on Windows
currentUser: createTestUser("Administrator", "S-1-5-21-123456789-123456789-123456789-500", "S-1-5-32-544", "C:\\Users\\Administrator"),
expectedIsProcessPrivileged: true,
expectedSupportsUserSwitching: true, // Windows supports user switching when privileged
},
{
name: "windows_regular_process",
os: "windows",
euid: 1000, // euid ignored on Windows
currentUser: createTestUser("alice", "1001", "1001", "C:\\Users\\alice"),
expectedIsProcessPrivileged: false,
expectedSupportsUserSwitching: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Inject platform dependencies and test REAL logic
cleanup := setupTestDependencies(tt.currentUser, nil, tt.os, tt.euid, nil, nil)
defer cleanup()
// Test the actual functions with injected dependencies
actualIsPrivileged := isCurrentProcessPrivileged()
actualSupportsUserSwitching := actualIsPrivileged
assert.Equal(t, tt.expectedIsProcessPrivileged, actualIsPrivileged,
"isCurrentProcessPrivileged() result mismatch")
assert.Equal(t, tt.expectedSupportsUserSwitching, actualSupportsUserSwitching,
"supportsUserSwitching() result mismatch")
t.Logf("Platform: %s, EUID: %d, User: %s", tt.os, tt.euid, tt.currentUser.Username)
t.Logf("Results: isPrivileged=%v, supportsUserSwitching=%v",
actualIsPrivileged, actualSupportsUserSwitching)
})
}
}
func TestCheckPrivileges_WindowsElevatedUserSwitching(t *testing.T) {
// Test Windows elevated user switching scenarios with simplified privilege logic
tests := []struct {
name string
currentUser *user.User
requestedUsername string
allowRoot bool
expectedAllowed bool
expectedErrorContains string
}{
{
name: "windows_admin_can_switch_to_alice",
currentUser: createTestUser("administrator", "S-1-5-21-123456789-123456789-123456789-500", "S-1-5-32-544", "C:\\\\Users\\\\Administrator"),
requestedUsername: "alice",
allowRoot: true,
expectedAllowed: true,
},
{
name: "windows_non_admin_can_try_switch",
currentUser: createTestUser("alice", "S-1-5-21-123456789-123456789-123456789-1001", "S-1-5-21-123456789-123456789-123456789-513", "C:\\\\Users\\\\alice"),
requestedUsername: "bob",
allowRoot: true,
expectedAllowed: true, // Privilege check allows it, OS will reject during execution
},
{
name: "windows_system_can_switch_to_alice",
currentUser: createTestUser("SYSTEM", "S-1-5-18", "S-1-5-18", "C:\\\\Windows\\\\system32\\\\config\\\\systemprofile"),
requestedUsername: "alice",
allowRoot: true,
expectedAllowed: true,
},
{
name: "windows_admin_root_not_allowed",
currentUser: createTestUser("administrator", "S-1-5-21-123456789-123456789-123456789-500", "S-1-5-32-544", "C:\\\\Users\\\\Administrator"),
requestedUsername: "root",
allowRoot: false,
expectedAllowed: false,
expectedErrorContains: "privileged user login is disabled",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup test dependencies with Windows OS and specified privileges
lookupUsers := map[string]*user.User{
tt.requestedUsername: createTestUser(tt.requestedUsername, "1002", "1002", "C:\\\\Users\\\\"+tt.requestedUsername),
}
cleanup := setupTestDependencies(tt.currentUser, nil, "windows", 1000, lookupUsers, nil)
defer cleanup()
server := &Server{allowRootLogin: tt.allowRoot}
result := server.CheckPrivileges(PrivilegeCheckRequest{
RequestedUsername: tt.requestedUsername,
FeatureSupportsUserSwitch: true,
FeatureName: "SSH login",
})
assert.Equal(t, tt.expectedAllowed, result.Allowed,
"Privilege check result should match expected for %s", tt.name)
if !tt.expectedAllowed && tt.expectedErrorContains != "" {
assert.NotNil(t, result.Error, "Should have error when not allowed")
assert.Contains(t, result.Error.Error(), tt.expectedErrorContains,
"Error should contain expected message")
}
if tt.expectedAllowed && tt.requestedUsername != "" && tt.currentUser.Username != tt.requestedUsername {
assert.True(t, result.RequiresUserSwitching, "Should require user switching for different user")
}
})
}
}