Source file src/os/user/user_windows_test.go

     1  // Copyright 2024 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package user
     6  
     7  import (
     8  	"crypto/rand"
     9  	"encoding/base64"
    10  	"encoding/binary"
    11  	"errors"
    12  	"fmt"
    13  	"internal/syscall/windows"
    14  	"internal/testenv"
    15  	"os"
    16  	"os/exec"
    17  	"runtime"
    18  	"slices"
    19  	"strconv"
    20  	"strings"
    21  	"sync"
    22  	"syscall"
    23  	"testing"
    24  	"unicode"
    25  	"unicode/utf8"
    26  	"unsafe"
    27  )
    28  
    29  // addUserAccount creates a local user account.
    30  // It returns the name and password of the new account.
    31  // Multiple programs or goroutines calling addUserAccount simultaneously will not choose the same directory.
    32  func addUserAccount(t *testing.T) (name, password string) {
    33  	t.TempDir()
    34  	pattern := t.Name()
    35  	// Windows limits the user name to 20 characters,
    36  	// leave space for a 4 digits random suffix.
    37  	const maxNameLen, suffixLen = 20, 4
    38  	pattern = pattern[:min(len(pattern), maxNameLen-suffixLen)]
    39  	// Drop unusual characters from the account name.
    40  	mapper := func(r rune) rune {
    41  		if r < utf8.RuneSelf {
    42  			if '0' <= r && r <= '9' ||
    43  				'a' <= r && r <= 'z' ||
    44  				'A' <= r && r <= 'Z' {
    45  				return r
    46  			}
    47  		} else if unicode.IsLetter(r) || unicode.IsNumber(r) {
    48  			return r
    49  		}
    50  		return -1
    51  	}
    52  	pattern = strings.Map(mapper, pattern)
    53  
    54  	// Generate a long random password.
    55  	var pwd [33]byte
    56  	rand.Read(pwd[:])
    57  	// Add special chars to ensure it satisfies password requirements.
    58  	password = base64.StdEncoding.EncodeToString(pwd[:]) + "_-As@!%*(1)4#2"
    59  	password16, err := syscall.UTF16PtrFromString(password)
    60  	if err != nil {
    61  		t.Fatal(err)
    62  	}
    63  
    64  	try := 0
    65  	for {
    66  		// Calculate a random suffix to append to the user name.
    67  		var suffix [2]byte
    68  		rand.Read(suffix[:])
    69  		suffixStr := strconv.FormatUint(uint64(binary.LittleEndian.Uint16(suffix[:])), 10)
    70  		name := pattern + suffixStr[:min(len(suffixStr), suffixLen)]
    71  		name16, err := syscall.UTF16PtrFromString(name)
    72  		if err != nil {
    73  			t.Fatal(err)
    74  		}
    75  		// Create user.
    76  		userInfo := windows.UserInfo1{
    77  			Name:     name16,
    78  			Password: password16,
    79  			Priv:     windows.USER_PRIV_USER,
    80  		}
    81  		err = windows.NetUserAdd(nil, 1, (*byte)(unsafe.Pointer(&userInfo)), nil)
    82  		if errors.Is(err, syscall.ERROR_ACCESS_DENIED) {
    83  			t.Skip("skipping test; don't have permission to create user")
    84  		}
    85  		// If the user already exists, try again with a different name.
    86  		if errors.Is(err, windows.NERR_UserExists) {
    87  			if try++; try < 1000 {
    88  				t.Log("user already exists, trying again with a different name")
    89  				continue
    90  			}
    91  		}
    92  		if err != nil {
    93  			t.Fatalf("NetUserAdd failed: %v", err)
    94  		}
    95  		// Delete the user when the test is done.
    96  		t.Cleanup(func() {
    97  			if err := windows.NetUserDel(nil, name16); err != nil {
    98  				if !errors.Is(err, windows.NERR_UserNotFound) {
    99  					t.Fatal(err)
   100  				}
   101  			}
   102  		})
   103  		return name, password
   104  	}
   105  }
   106  
   107  // windowsTestAccount creates a test user and returns a token for that user.
   108  // If the user already exists, it will be deleted and recreated.
   109  // The caller is responsible for closing the token.
   110  func windowsTestAccount(t *testing.T) (syscall.Token, *User) {
   111  	if testenv.Builder() == "" {
   112  		// Adding and deleting users requires special permissions.
   113  		// Even if we have them, we don't want to create users on
   114  		// on dev machines, as they may not be cleaned up.
   115  		// See https://dev.go/issue/70396.
   116  		t.Skip("skipping non-hermetic test outside of Go builders")
   117  	}
   118  	name, password := addUserAccount(t)
   119  	name16, err := syscall.UTF16PtrFromString(name)
   120  	if err != nil {
   121  		t.Fatal(err)
   122  	}
   123  	pwd16, err := syscall.UTF16PtrFromString(password)
   124  	if err != nil {
   125  		t.Fatal(err)
   126  	}
   127  	domain, err := syscall.UTF16PtrFromString(".")
   128  	if err != nil {
   129  		t.Fatal(err)
   130  	}
   131  	const LOGON32_PROVIDER_DEFAULT = 0
   132  	const LOGON32_LOGON_INTERACTIVE = 2
   133  	var token syscall.Token
   134  	if err = windows.LogonUser(name16, domain, pwd16, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token); err != nil {
   135  		t.Fatal(err)
   136  	}
   137  	t.Cleanup(func() {
   138  		token.Close()
   139  	})
   140  	usr, err := Lookup(name)
   141  	if err != nil {
   142  		t.Fatal(err)
   143  	}
   144  	return token, usr
   145  }
   146  
   147  func TestImpersonatedSelf(t *testing.T) {
   148  	runtime.LockOSThread()
   149  	defer runtime.UnlockOSThread()
   150  
   151  	want, err := current()
   152  	if err != nil {
   153  		t.Fatal(err)
   154  	}
   155  
   156  	levels := []uint32{
   157  		windows.SecurityAnonymous,
   158  		windows.SecurityIdentification,
   159  		windows.SecurityImpersonation,
   160  		windows.SecurityDelegation,
   161  	}
   162  	for _, level := range levels {
   163  		t.Run(strconv.Itoa(int(level)), func(t *testing.T) {
   164  			if err = windows.ImpersonateSelf(level); err != nil {
   165  				t.Fatal(err)
   166  			}
   167  			defer windows.RevertToSelf()
   168  
   169  			got, err := current()
   170  			if level == windows.SecurityAnonymous {
   171  				// We can't get the process token when using an anonymous token,
   172  				// so we expect an error here.
   173  				if err == nil {
   174  					t.Fatal("expected error")
   175  				}
   176  				return
   177  			}
   178  			if err != nil {
   179  				t.Fatal(err)
   180  			}
   181  			compare(t, want, got)
   182  		})
   183  	}
   184  }
   185  
   186  func TestImpersonated(t *testing.T) {
   187  	runtime.LockOSThread()
   188  	defer runtime.UnlockOSThread()
   189  
   190  	want, err := current()
   191  	if err != nil {
   192  		t.Fatal(err)
   193  	}
   194  
   195  	// Create a test user and log in as that user.
   196  	token, _ := windowsTestAccount(t)
   197  
   198  	// Impersonate the test user.
   199  	if err = windows.ImpersonateLoggedOnUser(token); err != nil {
   200  		t.Fatal(err)
   201  	}
   202  	defer func() {
   203  		err = windows.RevertToSelf()
   204  		if err != nil {
   205  			// If we can't revert to self, we can't continue testing.
   206  			panic(err)
   207  		}
   208  	}()
   209  
   210  	got, err := current()
   211  	if err != nil {
   212  		t.Fatal(err)
   213  	}
   214  	compare(t, want, got)
   215  }
   216  
   217  func TestCurrentNetapi32(t *testing.T) {
   218  	if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
   219  		// Test that Current does not load netapi32.dll.
   220  		// First call Current.
   221  		Current()
   222  
   223  		// Then check if netapi32.dll is loaded.
   224  		netapi32, err := syscall.UTF16PtrFromString("netapi32.dll")
   225  		if err != nil {
   226  			fmt.Fprintf(os.Stderr, "error: %s\n", err.Error())
   227  			os.Exit(9)
   228  			return
   229  		}
   230  		mod, _ := windows.GetModuleHandle(netapi32)
   231  		if mod != 0 {
   232  			fmt.Fprintf(os.Stderr, "netapi32.dll is loaded\n")
   233  			os.Exit(9)
   234  			return
   235  		}
   236  		os.Exit(0)
   237  		return
   238  	}
   239  	exe := testenv.Executable(t)
   240  	cmd := testenv.CleanCmdEnv(exec.Command(exe, "-test.run=^TestCurrentNetapi32$"))
   241  	cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")
   242  	out, err := cmd.CombinedOutput()
   243  	if err != nil {
   244  		t.Fatalf("%v\n%s", err, out)
   245  	}
   246  }
   247  
   248  func TestGroupIdsTestUser(t *testing.T) {
   249  	// Create a test user and log in as that user.
   250  	_, user := windowsTestAccount(t)
   251  
   252  	gids, err := user.GroupIds()
   253  	if err != nil {
   254  		t.Fatal(err)
   255  	}
   256  
   257  	if err != nil {
   258  		t.Fatalf("%+v.GroupIds(): %v", user, err)
   259  	}
   260  	if !slices.Contains(gids, user.Gid) {
   261  		t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid)
   262  	}
   263  }
   264  
   265  var isSystemDefaultLCIDEnglish = sync.OnceValue(func() bool {
   266  	// GetSystemDefaultLCID()
   267  	// https://learn.microsoft.com/en-us/windows/win32/api/winnls/nf-winnls-getsystemdefaultlcid
   268  	r, _, _ := syscall.MustLoadDLL("kernel32.dll").MustFindProc("GetSystemDefaultLCID").Call()
   269  	lcid := uint32(r)
   270  
   271  	lcidLow := lcid & 0xFF
   272  	// 0x0409 is en-US
   273  	// 0x1000 is "Locale without assigned LCID"
   274  	return lcidLow == 0x00 || lcidLow == 0x09
   275  })
   276  
   277  var serviceAccounts = []struct {
   278  	sid  string
   279  	name string // name on english Windows
   280  }{
   281  	{"S-1-5-18", "NT AUTHORITY\\SYSTEM"},
   282  	{"S-1-5-19", "NT AUTHORITY\\LOCAL SERVICE"},
   283  	{"S-1-5-20", "NT AUTHORITY\\NETWORK SERVICE"},
   284  }
   285  
   286  func TestLookupServiceAccount(t *testing.T) {
   287  	t.Parallel()
   288  	for _, tt := range serviceAccounts {
   289  		t.Run(tt.name, func(t *testing.T) {
   290  			u, err := Lookup(tt.name)
   291  			if err != nil {
   292  				t.Logf("Lookup(%q): %v", tt.name, err)
   293  				if !isSystemDefaultLCIDEnglish() {
   294  					t.Skipf("test not supported on non-English Windows")
   295  				}
   296  				t.Fail()
   297  				return
   298  			}
   299  			if u.Uid != tt.sid {
   300  				t.Errorf("unexpected uid for %q; got %q, want %q", u.Name, u.Uid, tt.sid)
   301  			}
   302  			t.Logf("Lookup(%q): %q", tt.name, u.Username)
   303  		})
   304  	}
   305  }
   306  
   307  func TestLookupIdServiceAccount(t *testing.T) {
   308  	t.Parallel()
   309  	for _, tt := range serviceAccounts {
   310  		u, err := LookupId(tt.sid)
   311  		if err != nil {
   312  			t.Errorf("LookupId(%q): %v", tt.sid, err)
   313  			continue
   314  		}
   315  		if u.Gid != tt.sid {
   316  			t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
   317  		}
   318  		if u.Username != tt.name {
   319  			if isSystemDefaultLCIDEnglish() {
   320  				t.Errorf("unexpected user name for %q; got %q, want %q", u.Gid, u.Username, tt.name)
   321  			} else {
   322  				t.Logf("user name for %q: %q", u.Gid, u.Username)
   323  			}
   324  		}
   325  	}
   326  }
   327  
   328  func TestLookupGroupServiceAccount(t *testing.T) {
   329  	t.Parallel()
   330  	for _, tt := range serviceAccounts {
   331  		t.Run(tt.name, func(t *testing.T) {
   332  			g, err := LookupGroup(tt.name)
   333  			if err != nil {
   334  				t.Logf("LookupGroup(%q): %v", tt.name, err)
   335  				if !isSystemDefaultLCIDEnglish() {
   336  					t.Skipf("test not supported on non-English Windows")
   337  				}
   338  				t.Fail()
   339  				return
   340  			}
   341  			if g.Gid != tt.sid {
   342  				t.Errorf("unexpected gid for %q; got %q, want %q", g.Name, g.Gid, tt.sid)
   343  			}
   344  		})
   345  	}
   346  }
   347  
   348  func TestLookupGroupIdServiceAccount(t *testing.T) {
   349  	t.Parallel()
   350  	for _, tt := range serviceAccounts {
   351  		u, err := LookupGroupId(tt.sid)
   352  		if err != nil {
   353  			t.Errorf("LookupGroupId(%q): %v", tt.sid, err)
   354  			continue
   355  		}
   356  		if u.Gid != tt.sid {
   357  			t.Errorf("unexpected gid for %q; got %q, want %q", u.Name, u.Gid, tt.sid)
   358  		}
   359  	}
   360  }
   361  

View as plain text