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

View as plain text